Cocos2d-x开发者指南10:物理引擎

2015年03月22日 10:24 0 点赞 0 评论 更新于 2025-11-21 13:31

假设我们的游戏开发进展十分顺利,已经在游戏中添加了精灵对象和游戏机制,此时可能会觉得这已经是一款可供玩家游玩的游戏了。但实际上,事情并没有那么简单,因为接下来我们会发现,游戏还需要模拟现实世界,尤其是现实的物理环境。这涉及到碰撞检测、重力、弹力和摩擦力等物理原理,在开发中,实现这些功能的工具被称为物理引擎。在这一章中,我们将学习物理引擎以及如何使用它。下面,让我们探讨一下何时、何地以及为何要使用物理引擎。

是否真的需要物理引擎?

物理引擎听起来可能让人望而生畏,你或许会问:“我真的需要它吗?”别担心,物理引擎并非像藏在床下准备吓你一跳的怪物。也许你的需求非常简单,简单到不需要使用物理引擎,仅依靠节点对象、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);

虽然这个例子已经很简单了,但你可能仍然觉得它有些复杂。没关系,让我们仔细分析一下,其实并没有那么难。代码的创建步骤如下:

  1. 创建 PhysicsBody 对象。
  2. 创建 Sprite 精灵。
  3. Sprite 精灵对象应用 PhysicsBody 对象的属性。
  4. 创建一个监听器以响应 onContactBegin 事件。

逐步分析后,这些概念就变得容易理解了。理解以下术语和概念,将有助于你更深入地了解物理引擎的细节。

物理引擎术语和概念

刚体

PhysicsBody 对象包含了一个对象的物理属性,如质量、位置、自旋度、速度和衰减度等。它是形状的核心,只有当形状与 PhysicsBody 关联后,PhysicsBody 对象才具有形状。

材质

材质描述了材料的以下属性:

  • 密度:用于计算母体的质量属性。
  • 摩擦:用于进行物体间的相对运动。
  • 恢复系数:用于使物体反弹,取值范围一般在 0 到 1 之间。0 表示不反弹,1 表示完全反弹。

形状

形状描述了碰撞的几何属性,将形状绑定到刚体上,就定义了刚体的形状。必要时,可以为一个刚体关联多个形状,这是定义复杂形状的一种方式。每个形状都与一个 PhysicsMaterial 对象相关,并且具有以下属性:type(种类)、area(面积)、mass(质量)、moment(转矩)、offset(偏移量/重心)和 tag(标签)。下面对这些属性进行详细解释:

  • type:描述了一系列的形状,如圆形、矩形、多边形等。
  • area:用于计算刚体的质量,刚体的质量由密度和体积决定。
  • mass:刚体所含物质的量,可以通过两种方式测量:物体在给定力下获得的加速度大小,或者在引力场中物体受到的力的大小。
  • moment:决定了获得特定角加速度所需的转矩。
  • offset:在刚体的当前坐标中,相对于刚体重心的偏移量。
  • tag:方便开发者确定形状,就像可以为所有节点分配标签以进行辨识和更便捷的访问一样。

以下是不同形状的描述:

  • PhysicsShape:实现了 PhysicsShape 的基类。
  • PhysicsShapeCircle:实心圆,无法用该形状实现空心圆。
  • PhysicsShapePolygon:实心且外凸的多边形。
  • PhysicsShapeBox:外凸多边形的一种,即矩形。
  • PhysicsShapeEdgeSegment:线段形状。
  • PhysicsShapeEdgePolygon:空心多边形,由多个线段构成的多边形边缘。
  • PhysicsShapeEdgeBox:空心矩形形状,由四个线段组成的矩形边缘。
  • PhysicsShapeEdgeChain:链形形状,可以有效地将许多边缘联结起来。

连接/关节

连接(contacts)和关节(joint)对象描述了刚体相互关联的方式。

世界(World)

物理刚体被添加到一个名为“世界(World)”的容器中,这里也是进行物理模拟的场所。将 bodiesshapesconstraints 等对象添加到物理世界中,并将整个物理世界作为一个整体进行更新。物理世界决定了所有这些部件之间的互动方式,许多通过物理 API 实现的互动都与 PhysicsWorld 对象相关。

请记住这些术语,以便在后续使用时随时查阅。

物理世界和物理刚体

物理世界

PhysicsWorld 对象是进行物理模拟的核心部件,它与场景(Scene)紧密整合。我们可以通过一个例子来理解,想象你居住的房子,房子就像是物理世界,而房子里的食物、刀具、电器等就像是物理刚体(PhysicsBody)对象。在这个世界中,这些刚体相互作用,它们可能相互接触,并对接触做出反应。例如,用刀子切食物,刀子可能切到食物,也可能没切到,甚至这把刀子可能根本不适合做这个任务。

你可以使用以下方式创建一个包含 PhysicsWorldScene 对象:

auto scene = Scene::createWithPhysics();

每个物理世界(PhysicsWorld)都有与之相关的属性:

  • 重力(gravity):全局重力,应用于整个物理世界,默认值为 Vec2(0.0f, -98.0f)
  • 速度(speed):设定物理世界的速度,这里的速度指的是模拟世界运动的比率,默认值为 1.0。
  • 刷新率:设定物理世界的刷新率,即 EngineUpdateTimes/PhysicsWorldUpdateTimes 的比值。
  • 子步(substeps):设定物理世界中每次刷新的子步数量。

刷新物理世界的过程也被称为步进(stepping)。默认情况下,物理世界会自动刷新,这被称为“自动步进(auto stepping)”。你可以通过 PhysicsWorld::setAutoStep(false) 禁用自动步进,然后通过 PhysicsWorld::step(time) 手动刷新 PhysicsWorld。使用子步可以以比单一框架更精确的时间增量来刷新物理世界,从而实现对步进过程更细致的控制,包括更流畅的运动。

物理刚体

物理刚体(PhysicsBody)对象具有位置(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 normal:从一个形状指向另一个形状的单位向量。

你可以从碰撞中获取 PhysicsShape,进而获取刚体。例如:

bool onContactBegin(PhysicsContact& contact)
{
auto bodyA = contact.getShapeA()->getBody();
auto bodyB = contact.getShapeB()->getBody();
return true;
}

你可以通过使用接触监听器(contact listener)来访问碰撞。接触监听器支持多种事件:

  • begin:两个形状刚刚开始接触。从回调函数中返回 true,碰撞正常发生;返回 false,物理引擎会忽略该碰撞。若返回 falsepreSolve()postSolve() 回调函数将被禁止运行,但当两个形状停止重叠时,仍会收到一个单独的事件。
  • pre-solve:两个形状接触在一起。在回调函数中返回 false,物理引擎会忽略此次碰撞;返回 true,碰撞正常进行。此外,你可以使用 setRestitution()setSurfaceVelocity() 函数来忽略碰撞值,从而提供自定义的恢复系数、摩擦系数和表面速度值。
  • post-solve:两个形状相接触,且它们之间的碰撞已被处理。
  • separate:两个形状刚刚停止接触。

也可以使用 EventListenerPhysicsContactWithBodiesEventListenerPhysicsContactWithShapesEventListenerPhysicsContactWithGroup 来监听感兴趣的刚体、形状和组的事件。此外,还需要设定与物理接触相关的掩码,因为即使创建了相关的 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);

这里是在制作撞击 logo 时使用矩形查询的几个例子。

禁用物理引擎

使用内置的物理引擎是个不错的选择,它稳定且强大。然而,有时你可能想使用其他物理引擎,此时只需在 base/ccConfig.h 中禁用 CC_USE_PHYSICS 即可。