假设我们的游戏开发进行的非常顺利。游戏中添加了精灵对象、游戏机制。这时候我们开始觉得:是一个让人玩的游戏了。但其实没有那么简单,因为接下来我们会发现我们的游戏还需要模拟现实世界,这点我们该怎么做呢?尤其那些现实的物理环境。因为这个会涉及到碰撞检测、重力、弹力和摩擦力等等物理原理,在开发中这些叫做物理引擎。这一章我们就来学习物理引擎和如何使用物理引擎。让我们来探讨一下,我们什么时候、什么地点、为什么会使用物理引擎吧。
物理引擎很可怕,我真的需要它么?请告诉我不是这样的!
别逃走啊,物理引擎其实也不是个藏在你床下准备把你吓个半死的怪物。或许你的需求很简单,简单到并不需要使用物理引擎。或许节点对象、update()函数、Rect对象、containsPoint()函数或intersectsRect()函数这些就够你用的了。例如:
void update(float dt)
{
auto p = touch->getLocation();
auto rect = this->getBoundingBox();
if(rect.containsPoint(p))
{
// do something, intersection
}
}
上面这套系统能满足简单的需求,但却无法扩展。假如你有100个精灵,而所有的这些精灵都需要不断地更新,以检测与其他对象的重叠状况,那该怎么办呢?用上面的系统,这也是可以实现的,但是会严重消耗CPU的使用率并影响帧速率。你的游戏就没办法继续玩下去了。物理引擎(PhysicsEngine)帮我们解决了这些问题,并且它是可扩展的,还也不会对CPU造成过大压力。这或许看起来有点陌生,我们还是来看一个简单的例子。之后,我们还是来看一个简单的例子,力求将概念、术语以及实践结合起来。
// create a static PhysicsBody
auto physicsBody = PhysicsBody::createBox(Size(65.0f , 81.0f ), PhysicsMaterial(0.1f, 1.0f, 0.0f));
physicsBody->setDynamic(false);
// create a sprite
auto sprite = Sprite::create("whiteSprite.png");
sprite->setPosition(Vec2(400, 400));
// sprite will use physicsBody
sprite->setPhysicsBody(physicsBody);
//add contact event listener
auto contactListener = EventListenerPhysicsContact::create();
contactListener->onContactBegin = CC_CALLBACK_1(onContactBegin, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);
虽然上面这个例子已经很简单了,但你可能还是觉得它复杂得有点吓人?没关系,让我们来更仔细地分析一下它,我们会发现也没那么复杂的。代码中的创建步骤为:创建PhysicsBody对象。创建Sprite精灵。Sprite精灵对象应用PhysicsBody对象的属性。创建了一个监听器以响应onContactBegin事件。
一旦我们一步步地来分析,这些概念都变得容易理解了。理解下面这些术语和概念,会有利于你更好地了解物理引擎的所有细节:
物理引擎术语和概念
刚体
PhysicsBody对象包含了一个对象的物理属性。这些属性包括:质量、位置、自旋度、速度和衰减度。PhysicsBody对象是形状的核心。当你把形状和PhysicsBody关联后,PhysicsBody对象才能具有形状。
材质
材质描述了材料的以下属性:
密度:它被用于计算母体的质量属性。
摩擦:它被用于进行物体间的相对运动。
恢复系数:它被用于使物体反弹。恢复系数一般设为0到1之间。0说明不反弹,1说明完全反弹。
形状
形状描述了碰撞的几何属性。将形状绑定到刚体,就定义了一个刚体的形状。如果必要,你可以为一个刚体关联无数的形状,这是一种定义复杂形状的方式。每个形状都与一个PhysicsMaterial对象相关,并且拥有以下的属性:type(种类), area(面积), mass(质量), moment(转矩), offset(偏移量/重心)和tag(标签)。可能你还对它们中的某些还感到陌生:
type:描述了一系列的形状,例如圆形,矩形,多边形等。
area:用于计算刚体的质量。密度和体积决定了刚体的质量。
mass:刚体所含的物质的量,可以用两种方式进行测量:物体在给定的力下获得的加速度大小,或者在一个引力场中物体受到力的大小。
moment:决定了获得特定角加速度所需要的转矩。
offset:在刚体的当前坐标中,相对于刚体的重心所偏移的量。
tag:用以使开发者较容易地确定形状。你大概还能记得把?你可以为所有的节点都分配一个标签,以进行辨识和实现更容易的访问。
我们这样来描述不同的形状:
PhysicsShape:shapes(形状)实现了PhysicsShape的基类。
PhysicsShapeCircle:圆是实心的。你无法用圆(circle)形状来实现空心圆。
PhysicsShapePolygon:多边形(Polygon)形状是指实心的且外凸的多边形。
PhsicsShapeBox:矩形(Box)形状是外凸的多边形的一种。
PhysicsShapeEdgeSegment:一种线段的形状。
PhysicsShapePolygon:空心多边形。一种由多个线段构成的多边形的边缘。
PhysicsShapeEdgeBox:空心矩形形状。一种由四个线段组成的矩形的边缘。
PhysicsShapeEdgeChain链形形状(chain shape)可以有效地把许多边缘联结起来。
连接/关节
连接(contacts)和关节(joint)对象描述了刚体相互关联的方式。
World 世界
物理刚体被添加到一个叫世界(World)的容器里,这也是它们被模拟的场所。将bodies,shapes,constraints这些对象添加到物理世界中,将整个物理世界作为一个整体进行更新。物理世界决定了所有这些部件在一起的互动方式。其中,用物理API实现的许多互动都是与PhysicsWorld这个对象有关的。
此处有许多需要记住的东西。请把这些术语记在身边,待会用到的时候以便随时查阅。
物理世界和物理刚体
物理世界
物理世界(PhysicsWorld)对象是进行物理模拟时的一个核心部件。物理世界(PhysicsWorld)与场景(Scene)紧密整合在一起。让我们来看一个我们都会涉及到的例子吧。你住的房子里有厨房吗?你想这个问题的时候,就像是在想你的物理世界一样!现在,你的世界里拥有一些物理刚体(PhysicsBody)对象,就跟食物、刀具、电器这些东西一样!在这个世界中,这些刚体相互作用。它们相互接触,并且对相互的接触做出反应。例如:用刀子切开食物,并把它放到电器中。刀子切到食物了吗?可能切到了。也可能还没有。还可能这个刀子根本就不适合做这个。
你可以用下面的方式创建一个包含有PhysicsWorld的Scene对象:
1auto scene = Scene::createWithPhysics();
每一个物理世界(PhysicsWorld)都具有与之相关的属性:
-重力(gravity):全局重力,应用于整个物理世界。默认值为Vec2(0.0f,-98.0f)。 -速度(speed):设定了物理世界的速度。这里,速度指的是这个模拟世界运动的一种比率。默认值为1.0。 -刷新率:设定了物理世界的刷新率,这里刷新率指的是EngineUpdateTimes/PhysicsWorldUpdateTimes的比值。 -子步(substeps):设定了物理世界中每次刷新的子步数量。
刷新物理世界的过程也被称为步进(stepping)。按照默认设置,物理世界会不停地进行自动刷新。这被称为“自动步进(auto stepping)”,它会自动地进行。你可以通过设定PhysicsWorld::setAutoStep(false)禁用一个物理世界的auto step,然后通过设定PhysicsWorld::step(time)来手动刷新PhysicsWorld。substeps使用比单一框架更加精确的时间增量来刷新物理世界。使用它,我们可以实现更加细致地实现对步进过程的控制,包括更加流畅的运动。
物理刚体
物理刚体(PhyicsBody)对象具有位置(position)和速度(velocity)两个属性。你可以在PhysicsBody上应用forces、movement、damping和impulses。物理刚体可以是静态的,也可以是动态的。静态的刚体在模拟世界中不会移动,看起来就像它拥有无限大的质量一样。动态的刚体则是一种完全仿真的模拟。它可以被用户手动移动,但更常见的是它们受到力的作用而移动。动态刚体可以与所有的刚体类型发生碰撞。Cocos2d-x提供了Node::setPhysicsbody()来将物理刚体与一个节点对象关联在一起。
让我们来创建一个静态的物理刚体对象和5个动态的物理刚体对象,并且把它们都设为矩形:
auto physicsBody = PhysicsBody::createBox(Size(65.0f, 81.0f),
PhysicsMaterial(0.1f, 1.0f, 0.0f));
physicsBody->setDynamic(false);
//create a sprite
auto sprite = Sprite::create("whiteSprite.png");
sprite->setPosition(s_centre);
addChild(sprite);
//apply physicsBody to the sprite
sprite->setPhysicsBody(physicsBody);
//add five dynamic bodies
for (int i = 0; i < 5; ++i)
{
physicsBody = PhysicsBody::createBox(Size(65.0f, 81.0f),
PhysicsMaterial(0.1f, 1.0f, 0.0f));
//set the body isn't affected by the physics world's gravitational force
physicsBody->setGravityEnable(false);
//set initial velocity of physicsBody
physicsBody->setVelocity(Vec2(cocos2d::random(-500,500),
cocos2d::random(-500,500)));
physicsBody->setTag(DRAG_BODYS_TAG);
sprite = Sprite::create("blueSprite.png");
sprite->setPosition(Vec2(s_centre.x + cocos2d::random(-300,300),
s_centre.y + cocos2d::random(-300,300)));
sprite->setPhysicsBody(physicsBody);
addChild(sprite);
}
结果是,5个动态的物理刚体对象围绕在一个静态的物理刚体对象周围不停地发生碰撞。
碰撞
你是否经历过车祸?是否跟什么物体相撞过?就像车一样,物理刚体对象可以互相接触。当它们接触的时候,就发生了碰撞。当碰撞发生时,它可以被完全忽略,也可以引起一系列事件。
碰撞筛选
碰撞筛选允许你启用或者阻止形状之间碰撞的发生。物理引擎支持使用类型、组位掩码来筛选碰撞。
Cocos2d-x中支持32种碰撞类型。对于每个形状都可以指定其所属的类型。还可以指定有哪些类型可以与这个形状进行碰撞。这些是通过掩码来完成的。例如:
auto sprite1 = addSpriteAtPosition(Vec2(s_centre.x - 150,s_centre.y));
sprite1->getPhysicsBody()->setCategoryBitmask(0x02);// 0010
sprite1->getPhysicsBody()->setCollisionBitmask(0x01); // 0001
sprite1 = addSpriteAtPosition(Vec2(s_centre.x - 150,s_centre.y + 100));
sprite1->getPhysicsBody()->setCategoryBitmask(0x02);// 0010
sprite1->getPhysicsBody()->setCollisionBitmask(0x01); // 0001
auto sprite2 = addSpriteAtPosition(Vec2(s_centre.x + 150,s_centre.y),1);
sprite2->getPhysicsBody()->setCategoryBitmask(0x01);// 0001
sprite2->getPhysicsBody()->setCollisionBitmask(0x02); // 0010
auto sprite3 = addSpriteAtPosition(Vec2(s_centre.x + 150,s_centre.y + 100),2);
sprite3->getPhysicsBody()->setCategoryBitmask(0x03);// 0011
sprite3->getPhysicsBody()->setCollisionBitmask(0x03); // 0011
可以通过检测、类型比较和碰撞掩码,来确定碰撞的发生:
if ((shapeA->getCategoryBitmask() & shapeB->getCollisionBitmask()) == 0 || (shapeB->getCategoryBitmask() & shapeA->getCollisionBitmask()) == 0)
{
// shapes can't collide
ret = false;
}
碰撞组允许你指定一个综合组的索引。你可以让具有同一组之索引的形状全都一直碰撞(正索引)或者永不碰撞(负索引或零索引)。组指数不同的形状间进行的碰撞,可以根据类型和掩码来进行筛选。换句话说,组筛选比类型筛选的优先级更高。
连接/关节
还记得之前的术语吗?关节是一种把接触点联结在一起的方式。没错,你可以把它类比为自己身体上的关节。每一个关节都有一个从PhysicsJoint对象获得的定义。在两个不同的刚体之间,所有的关节都是联结在一起的。刚体可以是静态的。你可以使用joint->setCollisionEnable(false)来避免相关联的刚体相互碰撞。很多关节的定义需要你提供一些几何数据。很多情况下,关节由锚点来定义。其余的关节定义数据取决于关节的类型。
PhysicsJointFixed:固定关节在一个特定的点上,将两个刚体结合在了一起。如果要创建一些以后会断裂的复杂形状,固定关节是非常有用的。
PhysicsJointLimit:一种限制关节,它利用了两个刚体间最大距离,就如同两个刚体被绳子连在一起一样。
PhysicsJointPin:针式关节可以让两个刚体独立地围绕锚点进行旋转,就如同它们被钉在一起了一样。
PhysicsJointDistance:设定两个刚体间的固定距离。
PhysicsJointSpring:用弹簧来联结两个物理刚体
PhysicsJointGroove:将一个刚体连到线上,另一个连到点上。
PhysicsJointRotarySpring:与弹簧关节相似,但是增加了自旋
PhysicsJointRotaryLimit:与限制关节相似,但是增加了自旋
PhysicsJointRatchet:与套筒扳手的工作类似
PhysicsJointGear:使一对刚体的角速度比率保持是一个常数。
PhysicsJointMotor:使一对刚体的相对角速度保持是一个常数。
碰撞检测
碰撞(Contacts)是一种由物理引擎创建的用以管理两个形状间碰撞的对象。Contact对象会自动创建,而非由用户创建。这里有几个与之相关联的术语。
contact point:contact point是两个形状相接触的那个点。
contact normal:contact normal指的是从一个形状指向另一个形状的单位向量。
你可以从一个碰撞中获取PhysicsShape。从中,你可以获取刚体。
bool onContactBegin(PhysicsContact& contact)
{
auto bodyA = contact.getShapeA()->getBody();
auto bodyB = contact.getShapeB()->getBody();
return true;
}
你可以通过使用contact listener来访问碰撞。contact listener支持多种事件:begin,pre-solve,post-solve,以及separate。
begin:在这一步骤中,两个形状刚刚开始接触。从回调函数中返回true,可以使碰撞正常发生,若返回false,则物理引擎会将碰撞整个忽略掉。如果返回false值,preSolve()和postSolve()回调函数会被禁止运行,不过当两个形状停止重叠时,你仍然可以收到一个单独的事件。
pre-solve:在这一步骤中,两个形状接触在一起。如果在回调函数中返回false值,则物理引擎会忽略掉此次碰撞;若返回true,碰撞则会正常进行。此外,你可以使用setRestitution()和setSurfaceVelocity()函数来忽略碰撞值,这样就可以提供自定义的恢复系数、摩擦系数和表面速度值。
post-solve:两个形状相接触,而它们之间的碰撞已被处理。
separate:在这一步骤中,两个形状刚刚停止接触。
也可以使用EventListenerPhysicsContactWithBodies,EventListenerPhysicsContactWithShapes,EventListenerPhysicsContactWithGroup来监听你感兴趣的刚体、形状和组的一些事件。除此之外,你还需要设定与物理接触相关的掩码,因为就算你创建了相关的EventListener,碰撞事件还是不会在默认状态下被接收。
例如:
bool init()
{
//create a static PhysicsBody
auto sprite = addSpriteAtPosition(s_centre,1);
sprite->setTag(10);
sprite->getPhysicsBody()->setContactTestBitmask(0xFFFFFFFF);
sprite->getPhysicsBody()->setDynamic(false);
//adds contact event listener
auto contactListener = EventListenerPhysicsContact::create();
contactListener->onContactBegin = CC_CALLBACK_1(PhysicsDemoCollisionProcessing::onContactBegin, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);
schedule(CC_SCHEDULE_SELECTOR(PhysicsDemoCollisionProcessing::tick), 0.3f);
return true;
return false;
}
void tick(float dt)
{
auto sprite1 = addSpriteAtPosition(Vec2(s_centre.x + cocos2d::random(-300,300),
s_centre.y + cocos2d::random(-300,300)));
auto physicsBody = sprite1->getPhysicsBody();
physicsBody->setVelocity(Vec2(cocos2d::random(-500,500),cocos2d::random(-500,500)));
physicsBody->setContactTestBitmask(0xFFFFFFFF);
}
bool onContactBegin(PhysicsContact& contact)
{
auto nodeA = contact.getShapeA()->getBody()->getNode();
auto nodeB = contact.getShapeB()->getBody()->getNode();
if (nodeA && nodeB)
{
if (nodeA->getTag() == 10)
{
nodeB->removeFromParentAndCleanup(true);
}
else if (nodeB->getTag() == 10)
{
nodeA->removeFromParentAndCleanup(true);
}
}
//bodies can collide
return true;
}
查询
你有没有过站在一个地方往四周看的经历?你能看到离你近的东西,也能看到离你远的东西。你能判断出它们离你有多远。物理引擎提供类似的空间查询功能。PhysicsWorld对象目前支持的查询包括点查询、射线查询和矩形查询。
点查询
当你碰到什么东西,比如说你的桌子的时候,你可以将此认为是一个点查询的例子。这使你能够检查在一个点周围的一定距离内是否有形状存在。对于鼠标拾取和简单的传感器来说,点查询是非常有用的。你还可以找到在一个形状上离某定点最近的点,或者找到离某个点最近的形状。
射线查询
当你四处看的时候,在你视线内的某些物体肯定会引起你的注意。像这样的时候,你基本上就算是执行了一次射线查询。你不停地扫描,直到有什么有趣的东西让你停下来。你可以使用对某个形状使用射线查询来获取第一个交叉点。例如:
void tick(float dt)
{
Vec2 d(300 * cosf(_angle), 300 * sinf(_angle));
Vec2 point2 = s_centre + d;
if (_drawNode)
{
removeChild(_drawNode);
}
_drawNode = DrawNode::create();
Vec2 points[5];
int num = 0;
auto func = [&points, &num](PhysicsWorld& world,
const PhysicsRayCastInfo& info, void* data)->bool
{
if (num < 5)
{
points[num++] = info.contact;
}
return true;
};
s_currScene->getPhysicsWorld()->rayCast(func, s_centre, point2, nullptr);
_drawNode->drawSegment(s_centre, point2, 1, Color4F::RED);
for (int i = 0; i < num; ++i)
{
_drawNode->drawDot(points[i], 3, Color4F(1.0f, 1.0f, 1.0f, 1.0f));
}
addChild(_drawNode);
_angle += 1.5f * (float)M_PI / 180.0f;
}
矩形查询
矩形查询提供了一个大致检查区域中存在的形状的一种快捷方式。它非常容易实现:
auto func = [](PhysicsWorld& world, PhysicsShape& shape, void* userData)->bool
{
//Return true from the callback to continue rect queries
return true;
}
scene->getPhysicsWorld()->queryRect(func, Rect(0,0,200,200), nullptr);
A few examples of using a Rect query while doing a logo smash:
这里是在制作撞击logo时使用矩形查询的几个例子:
禁用物理引擎
使用内置的物理引擎是个不错的想法。它又稳定又强大。然而,有时候你会想要使用一些其他的物理引擎。这时候你只需要在base/ccConfig.h中把CC_USE_PHYSICS禁用就好了。