Cocos2d之Box2d基础知识
前端之家收集整理的这篇文章主要介绍了
Cocos2d之Box2d基础知识,
前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
一、Box2d基础知识
1、关于
Box2D 是一个用于游戏的 2D 刚体仿真库。从游戏的视角来看,物理引擎就是一个程序性动画(proceduralanimation)的系统,而不是由动画师去移动你的物体。
1、核心概念
刚体(rigid body)
一块十分坚硬的物质,它上面的任何两点之间的距离都是完全不变的。
形状(shape)
一块严格依附于物体(body)的 2D 碰撞几何结构(collision geometry)。形状具有摩擦(friction)和恢
复(restitution)的材料性质。
约束(constraint)
一个约束(constraint)就是消除物体自由度的物理连接。在 2D 中,一个物体有 3 个自由度。如果我
们把一个物体钉在墙上(像摆锤那样),那我们就把它约束到了墙上。这样,此物体就只能绕着这个钉子旋
转,所以这个约束消除了它 2 个自由度。
接触约束(contact constraint)
一个防止刚体穿透,以及用于模拟摩擦(friction)和恢复(restitution)的特殊约束。你永远都不必创建
关节(joint)
它是一种用于把两个或多个物体固定到一起的约束。
Box2D
支持的关节类型有:旋转,棱柱,距离等
等。关节可以
支持限制(limits)和马达(motors)。
关节限制(joint limit)
一个关节限制(joint limit)限定了一个关节的运动范围。例如人类的胳膊肘只能做某一范围角度的运
动。
关节马达(joint motor)
一个关节马达能依照关节的自由度来驱动所连接的物体。例如,你可以使用一个马达来驱动一个肘的
旋转。
世界(world)
一个物理世界就是物体,形状和约束相互作用的集合。
Box2D
支持创建多个世界,但这通常是不必要
的。
3、创建一个世界
每个 Box2D 程序都将从一个世界对象(world object)的创建开始。这是一个管理内存,对象和模拟的中心。
要创建一个世界对象,我们首先需要定义一个世界的包围盒。
Box2D 使用包围盒来加速碰撞检测。尺寸并不关键,但合适的尺寸有助于
性能。这个包围盒过大总比过小好。
b2AABBworldAABB;
worldAABB.lowerBound.Set(-
100.0f,-
100.0f);
worldAABB.upperBound.Set(
100.0f);
接下来我们定义重力矢量。
b2Vec2gravity(
0.0f,128)">10.0f);
booldoSleep=
true;
//
当动态物体静止时使它休眠,减少性能开销
现在我们创建世界对象。
b2Worldworld(worldAABB,gravity,doSleep);//在栈上创建world
那么现在我们有了自己的物理世界,让我们再加些东西进去。
4、创建一个地面
第一步,我们创建地面体。要创建它我们需要一个物体定义(body definition),通过物体定义我们来指定地面体的初始位置。
b2BodyDefgroundBodyDef;
groundBodyDef.position.Set(
10.0f);
第二步,将物体定义传给世界对象来创建地面体。世界对象并不保存到物体定义的引用。地面体是作为静态物体(static body)创建的,静态物体之间并没有碰撞,它们是固定的。当一个物体具有零质量的时候 Box2D 就会确定它为静态物体,物体的默认质量是零,所以它们默认就是静态的。
b2Body*ground=world.CreateBody(&groundBodyDef);
第三步,我们创建一个地面的多边形定义。我们使用 SetAs
Box 简捷地把地面多边形规定为一个盒子(矩形)形状,盒子的中点就位于父物体的原点上。
b2PolygonDefgroundShapeDef;
groundShapeDef.SetAs
Box(
50.0f,128)">10.0f);
其中,SetAs
Box 函数接收了半个宽度和半个高度,这样的话,地面盒就是 100 个单位宽(x 轴)以及20 个单位高(y 轴)。
Box2D 已被调谐使用米,千克和秒来作单位,所以你可以用米来考虑长度。
在第四步中,我们在地面体上创建地面多边形,以完成地面体。
groundBody->CreateShape(&groundShapeDef);
创建形状用于碰撞检测等
5、创建一个动态物体
首先我们用 CreateBody 创建物体。
b2BodyDefbodyDef;
bodyDef.position.Set(
4.0f);
b2Body*body=world.CreateBody(&bodyDef);
接下来我们创建并添加一个多边形形状到物体上。注意我们把密度设置为 1,默认的密度是 0。并且,形状的摩擦设置到了 0.3。形状添加好以后,我们就使用 SetMassFromShapes 方法来命令物体通过形状去计算其自身的质量。这暗示了你可以给单个物体添加一个以上的形状。如果质量计算结果为 0,那么物体会变成真正的静态。
b2PolygonDefshapeDef;
shapeDef.SetAsBox(1.0f,128)">1.0f);
shapeDef.density=1.0f;
shapeDef.friction=0.3f;
body->CreateShape(&shapeDef);
body->SetMassFromShapes();
6、模拟(Box2D 的)世界
我们已经初始化好了地面盒和一个动态盒。现在我们只有少数几个问题需要考虑。Box2D 中有一些数学代码构成的积分器(integrator),积分器在离散的时间点上模拟物理方程,它将与游戏动画循环一同运行。所以我们需要为 Box2D 选取一个时间步,通常来说游戏物理引擎需要至少60Hz 的速度,也就是 1/60 的时间步。你可以使用更大的时间步,但是你必须更加小心地为你的世界调整定义。我们也不喜欢时间步变化得太大,所以不要把时间步关联到帧频(除非你真的必须这样做)。直截了当地,这个就是时间步:float32timeStep=1.0f/60.0f;
除了积分器之外,Box2D 中还有约束求解器(constraint solver)。约束求解器用于解决模拟中的所有约束,一次一个。单个的约束会被完美的求解,然而当我们求解一个约束的时候,我们就会稍微耽误另一个。要得到良好的解,我们需要迭代所有约束多次。建议的 Box2D 迭代次数是 10 次。你可以按自己的喜好去调整这个数,但要记得它是速度与质量之间的平衡。更少的迭代会增加性能并降低精度,同样地,更多的迭代会减少性能但提高模拟质量。这是我们选择的迭代次数:
int32iterations=10;//一个时间步遍历10次约束
现在我们可以开始模拟循环了,在游戏中模拟循环应该并入游戏循环。每次循环你都应该调用b2World::Step,通常调用一次就够了,这取决于帧频以及物理时间步。
这就是模拟 1 秒钟内 60 个时间步的循环
for(int32i=
0;i<
60;++i)
{
world.Step(timeStep,iterations);
}
7、API 设计
单位
Box2D 使用浮点数,所以必须使用一些公差来保证它正常工作。这些公差已经被调谐得适合米-千克-秒(MKS)单位。尤其是,Box2D 被调谐得能良好地处理 0.1 到 10 米之间的移动物体。这意味着从罐头盒到公共汽车大小的对象都能良好地工作。
• 注意:
Box2D 已被调谐至 MKS 单位。移动物体的尺寸大约应该保持在 0.1 到 10 米之间。你可能需要一些缩放系统来渲染你的场景和物体。
Box2D 中的例子是使用 OpenGL 的视口来变换的。
用户数据
b2Shape,b2Body 和 b2Joint 类都允许你通过一个 void 指针来附加
用户数据。这在你测试
Box2D数据结构,以及你想把它们联系到自己的引擎中的时候是较方便的。举个典型的例子,在角色上的刚体中附加到角色的指针,这就构成了一个循环引用。如果你有角色,你就能得到刚体。如果你有刚体,你就能得到角色。
GameActor*actor=GameCreateActor();
b2BodyDefbodyDef;
bodyDef.userData=actor;
actor->body=
Box2Dworld->CreateBody(&bodyDef);
• 使用碰撞结果给角色施加伤害
• 当玩家进入一个包围盒时播放一段脚本事件
• 当
Box2D
通知你一个关节即将摧毁时访问一个游戏结构
记得用户数据是可选的,并且能放入任何东西。然而,你需要保持一致性。例如,如果你想在一个物体中保存一个角色的指针,那你就应该在所有物体中都保存一个角色指针。不要在一个物体中保存角色指针,却在另一个物体中保存一个其它指针。这可能会导致程序崩溃。
8、世界
b2World 类包含着物体和关节。它管理着模拟的方方面面,并允许异步查询(就像 AABB 查询)。你与Box2D 的大部分交互都将通过 b2World 对象来完成。
要创建或摧毁一个世界你需要使用 new 和 delete:
b2World*myWorld=
newb2World(aabb,doSleep);
//
...dostuff...
deletemyWorld;
世界类用于驱动模拟。你需要指定一个时间步和一个迭代次数。例如:
float32timeStep=
1.0f/
60.f;
int32iterationCount=
10;
myWorld->Step(timeStep,iterationCount);
在时间步完成之后,你可以调查物体和关节的信息。最可能的情况是你会获取物体的位置,这样你才能更新你的角色并渲染它们。你可以在游戏循环的任何地方执行时间步,但你应该意识到事情发生的顺序。例如,如果你想要在一帧中得到新物体的碰撞结果,你必须在时间步之前创建物体。推荐使用固定的时间步。使用大一些的时间步你可以在低帧率的情况下提升性能。1/60 的时间步通常会呈现一个高质量的模拟。
扫描世界:
世界就是一个物体和关节的容器。你可以
获取世界中所有物体和关节并遍历它们。例如,这段
代码会唤醒世界中的所有物体:
for(b2Body*b=myWorld->GetBodyList();b;b=b->GetNext())
{
b->WakeUp();
}
AABB 查询:
有时你需要求出一个区域内的所有形状。b2World 类为此使用了 broad-phase 数据结构,提供了一个 log(N) 的
快速方法。你提供一个世界坐标的 AABB,而 b2World 会返回一个所有大概相交于此AABB 的形状之数组。这不是精确的,因为
函数实际上返回那些 AABB 与规定之 AABB 相交的形状。例如,下面的
代码找到所有大概与指定 AABB 相交的形状并唤醒所有关联的物体。
b2AABBaabb;
aabb.minVertex.Set(-
1.0f);
aabb.maxVertex.Set(
1.0f);
constint32k_bufferSize=
10;
b2Shape*buffer[k_bufferSize];
int32count=myWorld->Query(aabb,buffer,k_bufferSize);
for(int32i=
0;i<count;++i)
{
buffer[i]->GetBody()->WakeUp();
}
9、物体
物体具有位置和速度。你可以应用力,扭矩和冲量到物体。物体可以是静态的或动态的,静态物体永远不会移动,并且不会与其它静态物体发生碰撞。物体是形状的主干,物体携带形状在世界中运动。在 Box2D 中物体总是刚体,这意味着同一刚体上的两个形状永远不会相对移动。通常你会保存所有你所创建的物体的指针,这样你就能查询物体的位置,并在图形实体中更新它的位置。另外在不需要它们的时候你也需要通过它们的指针摧毁它们。
质量性质:
1)在物体定义中显式地设置
bodyDef.massData.mass=2.0f;物体的质量是2kg
2)显式地在物体上设置(在其创建之后)
3)基于物体上的形状来进行密度设置
b2PolygonDefshapeDef;
shapeDef.SetAs
Box(
1.0f);
shapeDef.density=
1.0f;
body->CreateShape(&shapeDef);
body->SetMassFromShapes();//这个
函数成本较高,所以你应该只在需要时使用它。
你可以在运行时调整一个物体的质量,这通常是在
添加或移除物体上之形状时完成的。可能你会根据物体上的当前形状来调整其质量。
可能你也会直接设置质量。例如,你可能会改变形状,但你只想使用自己的质量公式。
voidSetMass(
constb2MassData*massData);
float32GetMass()
const;
float32GetInertia()
const;
constb2Vec2&GetLocalCenter()
const;
位置和角度:
bodyDef.position.Set(
2.0f);
thebody'soriginposition.
bodyDef.angle=
0.25f*b2_pi;
thebody'sangleinradians.
你可以访问一个物体的位置和角度,这在你渲染相关游戏角色时很常用。你也可以设置位置,尽管这不怎么常用。
boolSetXForm(
constb2Vec2&position,float32angle);
constb2XForm&GetXForm()
constb2Vec2&GetPosition()
const;
float32GetAngle()
const;
你可以访问线速度与角速度,线速度是对于质心所言的。
voidSetLinearVelocity(
constb2Vec2&v);
b2Vec2GetLinearVelocity()
voidSetAngularVelocity(float32omega);
float32GetAngularVelocity()
const;
阻尼:
阻尼用于减小物体在世界中的速率。阻尼与摩擦是不同的,因为摩擦仅在物体有接触的时候才会发生,而阻尼的模拟要比摩擦便宜多了。然而,阻尼并不能取代摩擦,往往这两个
效果需要同时使用。阻尼参数的范围可以在 0 到无穷之间,0 的就是没有阻尼,无穷就是满阻尼。通常来说,阻尼的值应在 0 到 0.1 之间,我通常不使用线性阻尼,因为它会使物体看起来发飘。
bodyDef.linearDamping=
0.0f;
bodyDef.angularDamping=
0.01f;
阻尼相似于稳定性与
性能,阻尼值较小的时候阻尼效应几乎不依赖于时间步,而阻尼值较大的时候阻尼效应将随着时间步而变化。如果你使用固定的时间步(推荐)这就不是问题了。
休眠参数:
模拟物体的成本是高昂的,所以如果物体更少,那模拟的
效果就能更好。当一个物体停止了运动时,我们要停止去模拟它。当
Box2D 确定一个物体(或一组物体)已经停止移动时,物体就会进入休眠状态,消耗很小的
cpu 开销。如果一个醒着的物体接触到了一个休眠中的物体,那么休眠中的物体就会醒来。当物体上的关节或
触点被摧毁的时候,它们同样会醒来。你也可以手动地唤醒物体。通过物体定义,你可以指定一个物体是否可以休眠,或者创建一个休眠的物体。
bodyDef.allowSleep=
true;
bodyDef.isSleeping=
false;
子弹:
高速移动的物体在
Box2D 被称为子弹(bullet),你需要按照游戏的设计来决定哪些物体是子弹。如果你决定一个物体应该按照子弹去处理,使用下面的设置。
子弹开关只影响动态物体。
有的时候,在一个时间步内可能会有大量的刚体同时运动。如果一个物理引擎没有处理好大幅度运动的问题,你就可能会看见一些物体
错误地穿过了彼此。这种
效果被称为
隧道效应(tunneling)。默认情况下,
Box2D 会通过
连续碰撞检测(CCD)来防止动态物体穿越静态物体,这是通过从形状的旧位置到新位置的扫描来完成的。引擎会查找扫描中的新碰撞,并为这些碰撞计算碰撞时间(TOI)。物体会先被移动到它们的第一个 TOI,然后一直模拟到原时间步的结束。如果有必要这个步骤会重复执行。一般 CCD 不会应用于动态物体之间,这是为了保持
性能。在一些游戏环境中你需要在动态物体上也使用 CCD,譬如,你可能想用一颗高速的子弹去射击薄壁。没有 CCD,子弹就可能会隧穿薄壁。CCD 的成本是昂贵的,所以你可能不希望所有运动物体都成为子弹。所以
Box2D 默认只在动态物体和静态物体之间使用 CCD,这是防止物体逃脱游戏世界的一个有效
方法。然而,可能你有一些高速移动的物体需要一直使用 CCD。
状态信息:
物体的状态含有多个方面,通过这些
函数你可以访问这些状态数据:
boolIsBullet()
voidSetBullet(
boolflag);
boolIsStatic()
boolIsDynamic()
boolIsFrozen()
boolIsSleeping()
voidAllowSleeping(
voidWakeUp();
力和冲量:
你可以对一个物体应用力,扭矩,以及冲量。当应用一个力或冲量时,你需要提供一个世界位置。这常常会导致对质心的一个扭矩。
voidApplyForce(
constb2Vec2&force,constb2Vec2&point);
voidApplyTorque(float32torque);
voidApplyImpulse(
constb2Vec2&impulse,255)">constb2Vec2&point);
应用力,扭矩或冲量会唤醒物体,有时这是不合需求的。例如,你可能想要应用一个稳定的力,并允许物体休眠来提升
性能。这时,你可以使用这样的
代码:
if(myBody->IsSleeping()==
false)
{
myBody->ApplyForce(myForce,myPoint);
}
坐标转换:
物体类包含一些工具
函数,它们可以帮助你在局部和世界坐标系之间转换点和向量。如果你不了解这些概念,请看 Jim Van Verth 和 Lars Bishop 的“Essential Mathematics for Games and InteractiveApplications”。这些
函数都很高效,所以可放心使用。
b2Vec2GetWorldPoint(
constb2Vec2&localPoint);
b2Vec2GetWorldVector(
constb2Vec2&localVector);
b2Vec2GetLocalPoint(
constb2Vec2&worldPoint);
b2Vec2GetLocalVector(
constb2Vec2&worldVector);
列表
你可以遍历一个物体的形状,其主要用途是帮助你访问形状的
用户数据。
for(b2Shape*s=body->GetShapeList();s;s=s->GetNext())
{
MyShapeData*data=(MyShapeData*)s->GetUserData();
...
dosomethingwithdata...
}
你也可以用类似的方法遍历物体的关节列表。
10、形状
形状就是物体上的碰撞几何结构。另外形状也用于定义物体的质量。也就是说,你来指定密度,Box2D 可以帮你计算出质量。形状具有摩擦和恢复的性质。形状还可以携带筛选信息,使你可以防止某些游戏对象之间的碰撞。形状永远属于某物体,单个物体可以拥有多个形状。形状是抽象类,所以在 Box2D 中可以实现许多
类型的形状。如果你有勇气,那便可以实现出自己的形状类型(和碰撞算法)。
形状定义:
形状定义用于创建形状。通用的形状数据会保存在 b2ShapeDef 中,特殊的形状数据会保存在其派生类中。
1)摩擦和恢复
摩擦可以使对象逼真地沿其它对象滑动。Box2D 支持静摩擦和动摩擦,但使用相同的参数。摩擦参数经常会设置在 0 到 1 之间,0 意味着没有摩擦,1 会产生强摩擦。当计算两个形状之间的摩擦时,Box2D 必须联合两个形状的摩擦参数,这是通过以下公式完成的:
float32friction;
friction=sqrtf(shape1->friction*shape2->friction);
恢复可以使对象弹起,想象一下,在桌面上方丢下一个小球。恢复的值通常设置在 0 到 1 之间,0 的意思是小球不会弹起,这称为非弹性碰撞;1 的意思是小球的速度会得到精确的反射,这称为完全弹性碰撞。恢复是通过这样的公式计算的:
float32restitution;
restitution=b2Max(shape1->restitution,shape2->restitution);
当一个形状发生多碰撞时,恢复会被近似地模拟。这是因为 Box2D 使用了迭代求解器.
2)密度
Box2D 可以根据附加形状的质量分配来计算物体的质量以及转动惯量。直接指定物体质量常常会导致不协调的模拟。因此,推荐的
方法是使用b2Body::SetMassFromShape 来根据形状设置质量。
3)筛选
碰撞筛选是一个防止某些形状发生碰撞的系统。
Box2D
支持 16 个
种群,对于任何一个形状你都可以指定它属于哪个种群。你还可以指定这个形状可以和其它哪些种群发生碰撞。例如,你可以在一个多人游戏中指定玩家之间不会碰撞,怪物之间也不会碰撞,但是玩家和怪物会发生碰撞。这是通过掩码来完成的,例如:
playerShapeDef.filter.categoryBits=
0x0002;
monsterShapeDef.filter.categoryBits=
0x0004;
playerShape.filter.maskBits=
0x0004;
monsterShapeDef.filter.maskBits=
0x0002;
碰撞组可以让你指定一个整数的组索引。你可以让同一个组的所有形状总是相互碰撞(正索引)或永远不碰撞(负索引)。组索引通常用于一些以某种方式关联的事物,就像自行车的那些部件。在下面的例子中,shape1 和 shape2 总是碰撞,而 shape3 和 shape4 永远不会碰撞。
shape1Def.filter.groupIndex=
2;
shape2Def.filter.groupIndex=
2;
shape3Def.filter.groupIndex=-
8;
shape4Def.filter.groupIndex=-
8;
不同组索引之间形状的碰撞会按照种群和掩码来筛选。换句话说,组筛选比种群筛选有更高的优选权。
注意在
Box2D 中的其它碰撞筛选,这里是一个列表:
• 静态物体上的形状永远不会与另一个静态物体上的形状发生碰撞
• 同一个物体上的形状之间永远不会发生碰撞
• 你可以有选择地启用或
禁止由关节连接的物体上的形状之间是否碰撞
有时你可能希望在形状创建之后去改变其碰撞筛选,你可以使用 b2Shape::GetFilterData 以及b2Shape::SetFilterData 来存取已存在形状之 b2FilterData 结构。
Box2D 会缓存筛选结果,所以你需要使用 b2World::Refilter 手动地进行重筛选。
4)传感器
有时候游戏逻辑需要判断两个形状是否相交,但却不应该有碰撞反应。这可以通过传感器(sensor)来完成。传感器会侦测碰撞而不产生碰撞反应。你可以将任一形状标记为传感器,传感器可以是静态或动态的。记得,每个物体上可以有多个形状,并且传感器和实体形状是可以混合的。
myShapeDef.isSensor=true;
5)圆形定义
b2CircleDef 扩充了 b2ShapeDef 并
增加一个半径和一个局部位置。
b2CircleDefdef;
def.radius=
1.5f;
def.localPosition.Set(
0.0f);
6)多边形定义
b2PolyDef 用于定义凸多边形。要正确地使用需要一点点技巧,所以请仔细阅读。最大顶点数由b2_maxPolyVertices 定义,当前是 8。如果你需要更多顶点,你必须
修改 b2Settings.h 中的b2_maxPolyVertices。当创建多边形定义时,你需要给出所用的顶点数目。这些顶点必须按照相对于右手坐标系之 z 轴逆时
针(CCW)的顺序定义。在你的屏幕上可能是顺时针的,这取决于你的坐标系统规则。多边形必须是凸多边形,也就是,每个顶点都必须指向外面。最后,你也不应该重叠任何顶点。
Box2D 会
自动地封闭环路。
这里是一个三角形的多边形定义的例子:
b2PolygonDeftriangleDef;
triangleDef.vertexCount=
3;
triangleDef.vertices[
0].Set(-
0.0f);
triangleDef.vertices[
1].Set(
2].Set(
2.0f);
7)形状工厂
初始化一个形状定义,而后将其传递给父物体;形状就是这样创建的。
b2CircleDefcircleDef;
circleDef.radius=
3.0f;
circleDef.density=
2.5f;
b2Shape*myShape=myBody->CreateShape(&circleDef);
11、关节
关节的作用是把物体约束到世界,或约束到其它物体上。在游戏中的典型例子是木偶,跷跷板和滑轮。关节可以用许多种不同的方法结合起来,创造出有趣的运动。
有些关节提供了限制(limit),以便你控制运动范围。有些关节还提供了马达(motor),它可以以指定的速度驱动关节,直到你指定了更大的力或扭矩。
1)关节定义
各种关节类型都派生自 b2JointDef。所有关节都连接两个不同的物体,可能其中一个是静态物体。如果你想浪费内存的话,那就创建一个连接两个静态物体的关节
你可以为任何一种关节指定
用户数据。你还可以提供一个
标记,用于预防相连的物体发生碰撞。实际上,这是默认行为,你可以设置 collideConnected 布尔值来允许相连的物体碰撞。很多关节定义需要你提供一些几何数据。一个关节常常需要一个锚点(anchor point)来定义,这是固定于相接物体中的点。在
Box2D 中这点需要在局部坐标系中指定,这样,即便当前物体的变化违反了关节约束,关节还是可以被指定 —— 在游戏存取进度时这经常会发生。另外,有些关节定义需要默认的
物体之间的相对角度。这样才能通过关节限制或固定的相对角来正确地约束旋转。初始化几何数据可能有些乏味。所以很多关节提供了初始化
函数,消除了大部分工作。然而,这些初始化
函数通常只应用于原型,在产品
代码中应该直接地定义几何数据。这能使关节行为更加稳固。其余的关节定义数据依赖于关节的类型。下面我们来介绍它们。
2)距离关节
距离关节是最简单的关节之一,它描述了两个物体上的两个点之间的距离应该是常量。当你指定一个距离关节时,两个物体必须已在应有的位置上。随后,你指定两个世界坐标中的锚点。第一个锚点连接到物体 1,第二个锚点连接到物体 2。这些点隐含了距离约束的长度。
这是一个距离关节定义的例子。在此我们允许了碰撞。
b2DistanceJointDefjointDef;
jointDef.Initialize(myBody1,myBody2,worldAnchorOnBody1,
worldAnchorOnBody2);
jointDef.collideConnected=
true;
3)旋转关节
一个旋转关节会强制两个物体共享一个锚点,即所谓铰接点。旋转关节只有一个自由度:两个物体的相对旋转。这称之为关节角。
要指定一个旋转关节,你需要提供两个物体以及一个世界坐标的锚点。初始化函数会假定物体已经在应有位置了。在此例中,两个物体被旋转关节连接于第一个物体的质心。
b2RevoluteJointDefjointDef;
jointDef.Initialize(myBody1,myBody1->GetWorldCenter());
这里是对上面旋转关节定义的修订;这次,关节拥有一个限制以及一个马达,后者用于模拟摩擦。
b2RevoluteJointDefjointDef;
jointDef.Initialize(body1,body2,myBody1->GetWorldCenter());//使用 Initialize() 创建关节时,旋转关节角为 0,无论两个物体当前的角度怎样。
jointDef.lowerAngle=-
0.5f*b2_pi;
-90degrees最小角度
jointDef.upperAngle=
45degrees最大角度
jointDef.enableLimit=
true;
jointDef.maxMotorTorque=
10.0f;//马达
jointDef.motorSpeed=
0.0f;
jointDef.enableMotor=
true;
你可以访问旋转关节的角度,速度,以及扭矩。
float32GetJointAngle()
const
;
float32GetJointSpeed()const;
float32GetMotorTorque()const;
你也可以在每步中更新马达参数。
voidSetMotorSpeed(float32speed);
voidSetMaxMotorTorque(float32torque);
关节马达有一些有趣的能力。你可以在每个时间步中更新关节速度,这可以使关节像正弦波一样来回
...GameLoopBegin...
myJoint->SetMotorSpeed(cosf(
0.5f*time));
...GameLoopEnd...
你还可以使用关节马达来追踪某个关节角度。例如:
float32angleError=myJoint->GetJointAngle()-angleTarget;
float32gain=
0.1f;
myJoint->SetMotorSpeed(-gain*angleError);
...GameLoopEnd...
通常来讲你的增益参数不应过大,否则你的关节可能会变得不稳定。
4)移动关节
移动关节(prismatic joint)允许两个物体沿指定轴相对移动,它会阻止相对旋转。因此,移动关节只有一个自由度。
移动关节的定义有些类似于旋转关节;只是转动角度换成了平移,扭矩换成了力。以这样的类比,我们来看一个带有关节限制以及马达摩擦的移动关节定义:
b2PrismaticJointDefjointDef;
b2Vec2worldAxis(0.0f);
jointDef.Initialize(myBody1,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。移动关节的用法类似于旋转关节,这是它的相关成员函数:
float32GetJointTranslation()
const;
float32GetJointSpeed()
const;
float32GetMotorForce()
const;
voidSetMotorSpeed(float32speed);
voidSetMotorForce(float32force);
5)滑轮关节
滑轮关节用于创建理想的滑轮,它将两个物体接地(ground)并连接到彼此。这样,当一个物体升起时,另一个物体就会下降。滑轮的绳子长度取决于初始时的状态。
length1 + length2 == constant
你还可以提供一个系数(ratio)来模拟滑轮组,这会使滑轮一侧的运动比另一侧要快。同时,一侧的约束力也比另一侧要小。你也可以用这个来模拟机械杠杆(mechanical leverage)。length1 + ratio * length2 == constant举个例子,如果系数是 2,那么 length1 的变化会是 length2 的两倍。另外连接 body1 的绳子的约束力将会是连接 body2 绳子的一半。当滑轮的一侧完全展开时,另一侧的绳子长度为零,这可能会出问题。此时,约束方程将变得奇异。因此,滑轮关节约束了每一侧的最大长度。另外出于游戏原因你可能也希望控制这个最大长度。最大长度能提高稳定性,以及提供更多的控制。
这是一个滑轮定义的例子:
b2Vec2anchor1=myBody1->GetWorldCenter();
b2Vec2anchor2=myBody2->GetWorldCenter();
b2Vec2groundAnchor1(p1.x,p1.y+
10.0f);
b2Vec2groundAnchor2(p2.x,p2.y+
12.0f);
float32ratio=
1.0f;
b2PulleyJointDefjointDef;
jointDef.Initialize(myBody1,groundAnchor1,groundAnchor2,
anchor1,anchor2,ratio);
jointDef.maxLength1=
18.0f;
jointDef.maxLength2=
20.0f;
滑轮关节提供了当前长度:
float32GetLength1()
const;
float32GetLength2()
const;
6)齿轮关节
如果你想要创建复杂的机械装置,你可能需要齿轮。原则上,在 Box2D 中你可以用复杂的形状来模拟轮齿,但这并不十分高效,而且这样的工作可能有些乏味。另外,你还得小心地排列齿轮,保证轮齿能平稳地啮合。Box2D 提供了一个创建齿轮的更简单的方法:齿轮关节。
齿轮关节需要两个被旋转关节或移动关节接地(ground)的物体,你可以任意组合这些关节类型。另外,创建旋转或移动关节时,Box2D 需要地(ground)作为 body1。类似于滑轮的系数,你可以指定一个齿轮系数(ratio),齿轮系数可以为负。另外值得注意的是,当一个是旋转关节(有角度的)而另一个是移动关节(平移)时,齿轮系数是长度或长度分之一。coordinate1 + ratio * coordinate2 == constant这是一个齿轮关节的例子:
b2GearJointDefjointDef;
jointDef.body1=myBody1;
jointDef.body2=myBody2;
jointDef.joint1=myRevoluteJoint;
jointDef.joint2=myPrismaticJoint;
jointDef.ratio=
2.0f*b2_pi/myLength;
• 注意:齿轮关节总应该先于旋转或移动关节被
删除,否则你的
代码将会由于齿轮关节中的无效关节
指针而导致崩溃。另外齿轮关节也应该在任何相关物体被
删除之前
删除。
7)关节工厂
关节是通过世界的工厂方法来创建和摧毁的,这引出了一个旧问题:
• 注意:不要试图在栈上创建物体或关节,也不要使用 new 或 malloc 在堆上创建。物体以及关节必须要通过 b2World 类的
方法来创建或摧毁。
这是一个关于旋转关节生命期的例子:
b2RevoluteJointDefjointDef;
jointDef.body1=myBody1;
jointDef.body2=myBody2;
jointDef.anchorPoint=myBody1->GetCenterPosition();
b2RevoluteJoint*joint=myWorld->CreateJoint(&jointDef);
myWorld->DestroyJoint(joint);
joint=NULL;
8)使用关节
在许多模拟中,关节被创建之后便不再被访问了。然而,关节中包含着很多有用的数据,使你可以创建出丰富的模拟。首先,你可以在关节上得到物体,锚点,以及用户数据。
b2Body*GetBody1();
b2Body*GetBody2();
b2Vec2GetAnchor1();
b2Vec2GetAnchor2();
void*GetUserData();
11、接触
接触(contact)是由 Box2D 创建的用于管理形状间碰撞的对象。接触有不同的种类,它们都派生自b2Contact,用于管理不同类型形状之间的接触。例如,有管理多边形之间碰撞的类,有管理圆形之间碰撞的类。
触点(contact point)
两个形状相互接触的点。实际上当物体的表面相接触时可能会有一定接触区域,在
Box2D 则近似地
以少数点来接触。
接触向量(contact normal)
从 shape1 指向 shape2 的单位向量。
接触分隔(contact separation)
分隔相反于穿透,当形状相重叠时,分隔为负。可能以后的
Box2D 版本中会以正隔离来创建触点,所以当有触点的报告时你可能会检查符号。
法向力(normal force)
Box2D 使用了一个迭代接触求解器,并会以触点保存结果。你可以安全地使
用法向力来判断碰撞强度。例如,你可以使用这个力来引发破碎,或者播放碰撞的声音。
切向力(tangent force)
它是接触求解器关于摩擦力的估计量。
接触标识(contact ids)
Box2D 会试图利用一个时间步中的触点压力(contact force)结果来推测下一个时间步中的情况。接触标识用于匹配跨越时间步的触点,它包含了几何特征索引以便区分触点。
当两个形状的 AABB 重叠时,接触就被创建了。有时碰撞筛选会阻止接触的创建,有时尽管碰撞已筛选了 Box2D 还是须要创建一个接触,这种情况下它会使用 b2NullContact 来防止碰撞的发生。当AABB 不再重叠之后接触会被摧毁。也许你会皱起眉头,为了没有发生实际碰撞的形状(只是它们的 AABB)却创建了接触。好吧,的确是这样的,这是一个“鸡或蛋”的问题。我们并不知道是否需要一个接触,除非我们创建一个接触去分析碰撞。如果形状之间没有发生碰撞,我们需要正确地删除接触,或者,我们可以一直等到 AABB 不再重叠。Box2D 选择了后面这个方法。
1)接触监听器
通过实现 b2ContactListener 你就可以接受接触数据。当一个触点被创建时,当它持续超过一个时间步时,以及当它被摧毁时,这个监听器(listener)就会发出报告。请留意两个形状之间可能会有多个触点。
class MyContactListener : public b2ContactListener
{
public:
void Add(const b2ContactPoint* point)
{
// handle add point
}
void Persist(const b2ContactPoint* point)
{
// handle persist point
}
void Remove(const b2ContactPoint* point)
{
// handle remove point
}
void Result(const b2ContactResult* point)
{
// handle results
}
};
2)接触筛选
通常,你不希望游戏中的所有物体都发生碰撞。例如,你可能会创建一个只有某些角色才能通过的门。这称之为接触筛选,因为一些交互被筛选出了。
通过实现 b2ContactFilter 类,
Box2D 允许定制接触筛选。这个类需要一个 ShouldCollide
函数,用于接收两个 b2Shape 的指针,如果应该碰撞那么就返回 true。默认的 ShouldCollide 实现使用了 6 形状 中的 b2FilterData。
boolb2ContactFilter::ShouldCollide(b2Shape*shape1,b2Shape*shape2)
{
constb2FilterData&filter1=shape1->GetFilterData();
constb2FilterData&filter2=shape2->GetFilterData();
if(filter1.groupIndex==filter2.groupIndex&&filter1.groupIndex!=
0)
{
returnfilter1.groupIndex>
0;
}
boolcollide=(filter1.maskBits&filter2.categoryBits)!=
0&&
(filter1.categoryBits&filter2.maskBits)!=
0;
returncollide;
}
12、杂项
1)
你可以实现一个 b2BoundaryListener,这样当有物体超出世界的 AABB 时 b2World 就能
通知你。当你得到回调时,你不应该试图
删除物体;取而代之的是,你可以为角色做个
删除或
错误处理
标记,在物理时间步之后再进行这个事件的处理。
class MyBoundaryListener : public b2BoundaryListener
{
void Violation(b2Body* body)
{
MyActor* myActor = (MyActor*)body->GetUserData();
myActor->MarkForErrorHandling();
}
};
随后你可以在世界对象中
注册你的边界监听器实例,这应该安排在世界初始化过程中。
myWorld->SetListener(myBoundaryListener);
2)隐式摧毁
如果你摧毁一个
Box2D 实体,你应该保证所有到它的引用都
删除了。如果你只有实体的单个引用的话,那就简单了。但如果你有很多个引用,你可能要考虑实现一个处理类来封装原始指针。通常使用
Box2D 时你需要创建并摧毁许多物体,形状还有关节。管理这些实体有些
自动化,如果你摧毁一个物体,所有它的形状,关节,以及接触都会摧毁,这称为隐式摧毁。任何连接于这些关节或接触之一的物体将被唤醒,通常这是便利的。然而,你应该意识到了一个关键问题:
Box2D 提供了一个名为 b2WorldListener 的监听器类,你可以实现它并提供给世界对象,随后当关节将被隐式摧毁时世界对象就会提醒你。
你可以实现一个 b2DestructionListener,这样当一个形状或关节隐式摧毁时 b2World 就能
通知你,这可以帮助你预防访问无效指针。
class MyDestructionListener : public b2DestructionListener
{
void SayGoodbye(b2Joint* joint)
{
// remove all references to joint.
}
};
myWorld->SetListener(myDestructionListener);