一、box2d基础知识
1、关于
Box2D 是一个用于游戏的 2D 刚体仿真库。从游戏的视角来看,物理引擎就是一个程序性动画(procedural animation)的系统,而不是由动画师去移动你的物体。
1、核心概念
约束(constraint)
的。
3、 创建一个世界
每个 Box2D 程序都将从一个世界对象(world object)的创建开始。这是一个管理内存,对象和模拟的中心。
worldAABB.lowerBound.Set(-100.0f, -100.0f);
worldAABB.upperBound.Set(100.0f, 100.0f);
bool doSleep = true; //当动态物体静止时使它休眠,减少性能开销
现在我们创建世界对象。
b2World world(worldAABB, gravity, doSleep);//在栈上创建world
4、创建一个地面
第一步,我们创建地面体。要创建它我们需要一个物体定义(body definition),通过物体定义我们来指定地面体的初始位置。
groundBodyDef.position.Set(0.0f, -10.0f);
第二步,将物体定义传给世界对象来创建地面体。世界对象并不保存到物体定义的引用。地面体是作为静态物体(static body)创建的,静态物体之间并没有碰撞,它们是固定的。当一个物体具有零质量的时候 Box2D 就会确定它为静态物体,物体的默认质量是零,所以它们默认就是静态的。
groundShapeDef.SetAsBox(50.0f, 10.0f);
在第四步中,我们在地面体上创建地面多边形,以完成地面体。
bodyDef.position.Set(0.0f, 4.0f);
b2Body* body = world.CreateBody(&bodyDef);
接下来我们创建并添加一个多边形形状到物体上。注意我们把密度设置为 1,默认的密度是 0。并且,形状的摩擦设置到了 0.3。形状添加好以后,我们就使用 SetMassFromShapes 方法来命令物体通过形状去计算其自身的质量。这暗示了你可以给单个物体添加一个以上的形状。如果质量计算结果为 0,那么物体会变成真正的静态。
shapeDef.SetAsBox(1.0f, 1.0f);
shapeDef.density = 1.0f;
shapeDef.friction = 0.3f;
body->CreateShape(&shapeDef);
body->SetMassFromShapes();
6、模拟(Box2D 的)世界
我们已经初始化好了地面盒和一个动态盒。现在我们只有少数几个问题需要考虑。Box2D 中有一些数学代码构成的积分器(integrator),积分器在离散的时间点上模拟物理方程,它将与游戏动画循环一同运行。所以我们需要为 Box2D 选取一个时间步,通常来说游戏物理引擎需要至少 60Hz 的速度,也就是 1/60 的时间步。你可以使用更大的时间步,但是你必须更加小心地为你的世界调整定义。我们也不喜欢时间步变化得太大,所以不要把时间步关联到帧频(除非你真的必须这样做)。直截了当地,这个就是时间步:float32 timeStep = 1.0f / 60.0f;
除了积分器之外,Box2D 中还有约束求解器(constraint solver)。约束求解器用于解决模拟中的所有约束,一次一个。单个的约束会被完美的求解,然而当我们求解一个约束的时候,我们就会稍微耽误另一个。要得到良好的解,我们需要迭代所有约束多次。建议的 Box2D 迭代次数是 10 次。你可以按自己的喜好去调整这个数,但要记得它是速度与质量之间的平衡。更少的迭代会增加性能并降低精度,同样地,更多的迭代会减少性能但提高模拟质量。这是我们选择的迭代次数:
int32 iterations = 10;//一个时间步遍历10次约束
现在我们可以开始模拟循环了,在游戏中模拟循环应该并入游戏循环。每次循环你都应该调用 b2World::Step,通常调用一次就够了,这取决于帧频以及物理时间步。
这就是模拟 1 秒钟内 60 个时间步的循环
{
world.Step(timeStep, iterations);
}
7、API 设计
Box2D 使用浮点数,所以必须使用一些公差来保证它正常工作。这些公差已经被调谐得适合米-千克-秒(MKS)单位。尤其是,Box2D 被调谐得能良好地处理 0.1 到 10 米之间的移动物体。这意味着从罐头盒到公共汽车大小的对象都能良好地工作。
用户数据
b2BodyDef bodyDef;
bodyDef.userData = actor;
actor->body = box2Dworld->CreateBody(&bodyDef);
8、世界
b2World 类包含着物体和关节。它管理着模拟的方方面面,并允许异步查询(就像 AABB 查询)。你与 Box2D 的大部分交互都将通过 b2World 对象来完成。
要创建或摧毁一个世界你需要使用 new 和 delete:
// ... do stuff ...
delete myWorld;
世界类用于驱动模拟。你需要指定一个时间步和一个迭代次数。例如:
int32 iterationCount = 10;
myWorld->Step(timeStep, iterationCount);
在时间步完成之后,你可以调查物体和关节的信息。最可能的情况是你会获取物体的位置,这样你才能更新你的角色并渲染它们。你可以在游戏循环的任何地方执行时间步,但你应该意识到事情发生的顺序。例如,如果你想要在一帧中得到新物体的碰撞结果,你必须在时间步之前创建物体。推荐使用固定的时间步。使用大一些的时间步你可以在低帧率的情况下提升性能。1/60 的时间步通常会呈现一个高质量的模拟。
扫描世界:
{
b->WakeUp();
}
AABB 查询:
aabb.minVertex.Set(-1.0f, -1.0f);
aabb.maxVertex.Set(1.0f, 1.0f);
const int32 k_bufferSize = 10;
b2Shape *buffer[k_bufferSize];
int32 count = myWorld->Query(aabb, buffer, k_bufferSize);
for (int32 i = 0; i < count; ++i)
{
buffer[i]->GetBody()->WakeUp();
}
9、 物体
物体具有位置和速度。你可以应用力,扭矩和冲量到物体。物体可以是静态的或动态的,静态物体永远不会移动,并且不会与其它静态物体发生碰撞。物体是形状的主干,物体携带形状在世界中运动。在 Box2D 中物体总是刚体,这意味着同一刚体上的两个形状永远不会相对移动。通常你会保存所有你所创建的物体的指针,这样你就能查询物体的位置,并在图形实体中更新它的位置。另外在不需要它们的时候你也需要通过它们的指针摧毁它们。
质量性质:
1)在物体定义中显式地设置
bodyDef.massData.mass = 2.0f;//物体的质量是2kg
2)显式地在物体上设置(在其创建之后)
3)基于物体上的形状来进行密度设置
b2PolygonDef shapeDef;
shapeDef.density = 1.0f;
body->CreateShape(&shapeDef);
body->SetMassFromShapes();//这个函数成本较高,所以你应该只在需要时使用它。
float32 GetInertia() const;
const b2Vec2& GetLocalCenter() const;
位置和角度:
bodyDef.angle = 0.25f * b2_pi; // the body's angle in radians.
const b2XForm& GetXForm() const;
const b2Vec2& GetPosition() const;
float32 GetAngle() const;
你可以访问线速度与角速度,线速度是对于质心所言的。
b2Vec2 GetLinearVelocity() const;
void SetAngularVelocity(float32 omega);
float32 GetAngularVelocity() const;
阻尼:
bodyDef.angularDamping = 0.01f;
休眠参数:
bodyDef.isSleeping = false;
子弹:
状态信息:
void SetBullet(bool flag);
bool IsStatic() const;
bool IsDynamic() const;
bool IsFrozen() const;
bool IsSleeping() const;
void AllowSleeping(bool flag);
void WakeUp();
力和冲量:
void ApplyTorque(float32 torque);
void ApplyImpulse(const b2Vec2& impulse, const b2Vec2& point);
{
myBody->ApplyForce(myForce, myPoint);
}
坐标转换:
b2Vec2 GetWorldVector(const b2Vec2& localVector);
b2Vec2 GetLocalPoint(const b2Vec2& worldPoint);
b2Vec2 GetLocalVector(const b2Vec2& worldVector);
列表
{
MyShapeData* data = (MyShapeData*)s->GetUserData();
... do something with data ...
}
你也可以用类似的方法遍历物体的关节列表。
10、 形状
形状就是物体上的碰撞几何结构。另外形状也用于定义物体的质量。也就是说,你来指定密度,Box2D 可以帮你计算出质量。形状具有摩擦和恢复的性质。形状还可以携带筛选信息,使你可以防止某些游戏对象之间的碰撞。形状永远属于某物体,单个物体可以拥有多个形状。形状是抽象类,所以在 Box2D 中可以实现许多
形状定义 :
形状定义用于创建形状。通用的形状数据会保存在 b2ShapeDef 中,特殊的形状数据会保存在其派生类中。
1)摩擦和恢复
摩擦可以使对象逼真地沿其它对象滑动。Box2D 支持静摩擦和动摩擦,但使用相同的参数。摩擦参数经常会设置在 0 到 1 之间,0 意味着没有摩擦,1 会产生强摩擦。当计算两个形状之间的摩擦时,Box2D 必须联合两个形状的摩擦参数,这是通过以下公式完成的:
friction = sqrtf(shape1->friction * shape2->friction);
恢复可以使对象弹起,想象一下,在桌面上方丢下一个小球。恢复的值通常设置在 0 到 1 之间,0 的意思是小球不会弹起,这称为非弹性碰撞;1 的意思是小球的速度会得到精确的反射,这称为完全弹性碰撞。恢复是通过这样的公式计算的:
restitution = b2Max(shape1->restitution, shape2->restitution);
当一个形状发生多碰撞时,恢复会被近似地模拟。这是因为 Box2D 使用了迭代求解器.
2) 密度
3) 筛选
碰撞筛选是一个防止某些形状发生碰撞的系统。
monsterShapeDef.filter.categoryBits = 0x0004;
playerShape.filter.maskBits = 0x0004;
monsterShapeDef.filter.maskBits = 0x0002;
碰撞组可以让你指定一个整数的组索引。你可以让同一个组的所有形状总是相互碰撞(正索引)或永远不碰撞(负索引)。组索引通常用于一些以某种方式关联的事物,就像自行车的那些部件。在下面的例子中,shape1 和 shape2 总是碰撞,而 shape3 和 shape4 永远不会碰撞。
shape2Def.filter.groupIndex = 2;
shape3Def.filter.groupIndex = -8;
shape4Def.filter.groupIndex = -8;
不同组索引之间形状的碰撞会按照种群和掩码来筛选。换句话说,组筛选比种群筛选有更高的优选权。
4)传感器
有时候游戏逻辑需要判断两个形状是否相交,但却不应该有碰撞反应。这可以通过传感器(sensor)来完成。传感器会侦测碰撞而不产生碰撞反应。你可以将任一形状标记为传感器,传感器可以是静态或动态的。记得,每个物体上可以有多个形状,并且传感器和实体形状是可以混合的。
myShapeDef.isSensor = true;
5) 圆形定义
def.radius = 1.5f;
def.localPosition.Set(1.0f, 0.0f);
6)多边形定义
这里是一个三角形的多边形定义的例子:
triangleDef.vertexCount = 3;
triangleDef.vertices[0].Set(-1.0f, 0.0f);
triangleDef.vertices[1].Set(1.0f, 0.0f);
triangleDef.vertices[2].Set(0.0f, 2.0f);
7)形状工厂
初始化一个形状定义,而后将其传递给父物体;形状就是这样创建的。
circleDef.radius = 3.0f;
circleDef.density = 2.5f;
b2Shape* myShape = myBody->CreateShape(&circleDef);
11、关节
关节的作用是把物体约束到世界,或约束到其它物体上。在游戏中的典型例子是木偶,跷跷板和滑轮。关节可以用许多种不同的方法结合起来,创造出有趣的运动。
2)距离关节
距离关节是最简单的关节之一,它描述了两个物体上的两个点之间的距离应该是常量。当你指定一个距离关节时,两个物体必须已在应有的位置上。随后,你指定两个世界坐标中的锚点。第一个锚点连接到物体 1,第二个锚点连接到物体 2。这些点隐含了距离约束的长度。
这是一个距离关节定义的例子。在此我们允许了碰撞。
jointDef.Initialize(myBody1, myBody2, worldAnchorOnBody1,
worldAnchorOnBody2);
jointDef.collideConnected = true;
3)旋转关节
一个旋转关节会强制两个物体共享一个锚点,即所谓铰接点。旋转关节只有一个自由度:两个物体的相对旋转。这称之为关节角。
要指定一个旋转关节,你需要提供两个物体以及一个世界坐标的锚点。初始化函数会假定物体已经在应有位置了。在此例中,两个物体被旋转关节连接于第一个物体的质心。
jointDef.Initialize(myBody1, myBody2, myBody1->GetWorldCenter());
这里是对上面旋转关节定义的修订;这次,关节拥有一个限制以及一个马达,后者用于模拟摩擦。
jointDef.Initialize(body1, body2, myBody1->GetWorldCenter());//使用 Initialize() 创建关节时,旋转关节角为 0,无论两个物体当前的角度怎样。
jointDef.lowerAngle = -0.5f * b2_pi; // -90 degrees最小角度
jointDef.upperAngle = 0.25f * b2_pi; // 45 degrees最大角度
jointDef.enableLimit = true;
jointDef.maxMotorTorque = 10.0f;//马达
jointDef.motorSpeed = 0.0f;
jointDef.enableMotor = true;
你可以访问旋转关节的角度,速度,以及扭矩。
float32 GetJointSpeed() const;
float32 GetMotorTorque() const;
你也可以在每步中更新马达参数。
关节马达有一些有趣的能力。你可以在每个时间步中更新关节速度,这可以使关节像正弦波一样来回
myJoint->SetMotorSpeed(cosf(0.5f * time));
// ... Game Loop End ...
float32 angleError = myJoint->GetJointAngle() - angleTarget;
float32 gain = 0.1f;
myJoint->SetMotorSpeed(-gain * angleError);
// ... Game Loop End ...
4)移动关节
移动关节(prismatic joint)允许两个物体沿指定轴相对移动,它会阻止相对旋转。因此,移动关节只有一个自由度。
b2Vec2 worldAxis(1.0f, 0.0f);
jointDef.Initialize(myBody1, myBody2, myBody1->GetWorldCenter(),
worldAxis);
jointDef.lowerTranslation = -5.0f;
jointDef.upperTranslation = 2.5f;
jointDef.enableLimit = true;
jointDef.motorForce = 1.0f;
jointDef.motorSpeed = 0.0f;
jointDef.enableMotor = true;
旋转关节隐含着一个从屏幕射出的轴,而移动关节明确地需要一个平行于屏幕的轴。这个轴会固定于两个物体之上,沿着它们的运动方向。就像旋转关节一样,当使用 Initialize() 创建移动关节时,移动为 0。所以一定要确保移动限制范围内包含了 0。移动关节的用法类似于旋转关节,这是它的相关成员函数:
float32 GetJointSpeed() const;
float32 GetMotorForce() const;
void SetMotorSpeed(float32 speed);
void SetMotorForce(float32 force);
5)滑轮关节
滑轮关节用于创建理想的滑轮,它将两个物体接地(ground)并连接到彼此。这样,当一个物体升起时,另一个物体就会下降。滑轮的绳子长度取决于初始时的状态。
你还可以提供一个系数(ratio)来模拟滑轮组,这会使滑轮一侧的运动比另一侧要快。同时,一侧的约束力也比另一侧要小。你也可以用这个来模拟机械杠杆(mechanical leverage)。length1 + ratio * length2 == constant 举个例子,如果系数是 2,那么 length1 的变化会是 length2 的两倍。另外连接 body1 的绳子的约束力将会是连接 body2 绳子的一半。当滑轮的一侧完全展开时,另一侧的绳子长度为零,这可能会出问题。此时,约束方程将变得奇异。因此,滑轮关节约束了每一侧的最大长度。另外出于游戏原因你可能也希望控制这个最大长度。最大长度能提高稳定性,以及提供更多的控制。
b2Vec2 anchor2 = myBody2->GetWorldCenter();
b2Vec2 groundAnchor1(p1.x, p1.y + 10.0f);
b2Vec2 groundAnchor2(p2.x, p2.y + 12.0f);
float32 ratio = 1.0f;
b2PulleyJointDef jointDef;
jointDef.Initialize(myBody1, myBody2, groundAnchor1, groundAnchor2,
anchor1, anchor2, ratio);
jointDef.maxLength1 = 18.0f;
jointDef.maxLength2 = 20.0f;
滑轮关节提供了当前长度:
6) 齿轮关节
如果你想要创建复杂的机械装置,你可能需要齿轮。原则上,在 Box2D 中你可以用复杂的形状来模拟轮齿,但这并不十分高效,而且这样的工作可能有些乏味。另外,你还得小心地排列齿轮,保证轮齿能平稳地啮合。Box2D 提供了一个创建齿轮的更简单的方法:齿轮关节。
齿轮关节需要两个被旋转关节或移动关节接地(ground)的物体,你可以任意组合这些关节类型。另外,创建旋转或移动关节时,Box2D 需要地(ground)作为 body1。类似于滑轮的系数,你可以指定一个齿轮系数(ratio),齿轮系数可以为负。另外值得注意的是,当一个是旋转关节(有角度的)而另一个是移动关节(平移)时,齿轮系数是长度或长度分之一。coordinate1 + ratio * coordinate2 == constant这是一个齿轮关节的例子:
jointDef.body1 = myBody1;
jointDef.body2 = myBody2;
jointDef.joint1 = myRevoluteJoint;
jointDef.joint2 = myPrismaticJoint;
jointDef.ratio = 2.0f * b2_pi / myLength;
7)关节工厂
关节是通过世界的工厂方法来创建和摧毁的,这引出了一个旧问题:
jointDef.body1 = myBody1;
jointDef.body2 = myBody2;
jointDef.anchorPoint = myBody1->GetCenterPosition();
b2RevoluteJoint* joint = myWorld->CreateJoint(&jointDef);
// ... do stuff ...
myWorld->DestroyJoint(joint);
joint = NULL;
8)使用关节
在许多模拟中,关节被创建之后便不再被访问了。然而,关节中包含着很多有用的数据,使你可以创建出丰富的模拟。首先,你可以在关节上得到物体,锚点,以及用户数据。
b2Body* GetBody2();
b2Vec2 GetAnchor1();
b2Vec2 GetAnchor2();
void* GetUserData();
11、接触
接触(contact)是由 Box2D 创建的用于管理形状间碰撞的对象。接触有不同的种类,它们都派生自 b2Contact,用于管理不同类型形状之间的接触。例如,有管理多边形之间碰撞的类,有管理圆形之间碰撞的类。
触点(contact point)
切向力(tangent force)
当两个形状的 AABB 重叠时,接触就被创建了。有时碰撞筛选会阻止接触的创建,有时尽管碰撞已筛选了 Box2D 还是须要创建一个接触,这种情况下它会使用 b2NullContact 来防止碰撞的发生。当 AABB 不再重叠之后接触会被摧毁。也许你会皱起眉头,为了没有发生实际碰撞的形状(只是它们的 AABB)却创建了接触。好吧,的确是这样的,这是一个“鸡或蛋”的问题。我们并不知道是否需要一个接触,除非我们创建一个接触去分析碰撞。如果形状之间没有发生碰撞,我们需要正确地删除接触,或者,我们可以一直等到 AABB 不再重叠。Box2D 选择了后面这个方法。
{
const b2FilterData& filter1 = shape1->GetFilterData();
const b2FilterData& filter2 = shape2->GetFilterData();
if (filter1.groupIndex == filter2.groupIndex && filter1.groupIndex != 0)
{
return filter1.groupIndex > 0;
}
bool collide = (filter1.maskBits & filter2.categoryBits) != 0 &&
(filter1.categoryBits & filter2.maskBits) != 0;
return collide;
}