精通Cocos2d-x游戏开发(进阶卷)
上QQ阅读APP看书,第一时间看更新

9.5 关节Joint

关节,或者叫连接器,是用来连接两个刚体的对象,关节是一个很形象的比喻,就像人们身上的关节,如膝关节。在我们身边还有很多这样的例子,如可以推开的门、眼镜的镜架、汽车的轮子,这些把两个物体连接在一起,又在一定范围内可以移动或者旋转的对象,都可以称之为关节,关节是一种约束对象,对连接的两个物体进行约束。Box2d提供了下面10种关节。

        enum b2JointType
        {
            e_unknownJoint,
            e_revoluteJoint,            //旋转关节
            e_prismaticJoint,           //平移关节
            e_distanceJoint,            //距离关节
            e_pulleyJoint,              //滑轮关节
            e_mouseJoint,               //鼠标关节
            e_gearJoint,                //齿轮关节
            e_wheelJoint,               //滚轮关节
            e_weldJoint,                //焊接关节
            e_frictionJoint,            //摩擦关节
            e_ropeJoint                 //绳索关节
        };

9.5.1 使用关节

在详细介绍每个关节之前,先来看一下在代码中如何使用关节。创建一个关节的步骤和创建刚体十分相似,都是先填充一个关节定义结构,然后放到World中,由World来创建这个关节,每个关节都必须包含两个刚体。

对于不同的关节,需要在userData中设置关节所需的信息,World的CreateJoint返回一个b2Joint指针,但b2Joint只是一个抽象类,World返回的b2Joint实际是一个b2DistanceJoint或b2RevoluteJoint之类的对象。

        struct b2JointDef
        {
            b2JointDef()
            {
              type = e_unknownJoint;
              userData = NULL;
              bodyA = NULL;
              bodyB = NULL;
              collideConnected = false;
          }
          ///关节的类型
          b2JointType type;
          ///特殊数据
          void* userData;
          ///第一个刚体
          b2Body* bodyA;
          ///第二个刚体
          b2Body* bodyB;
          ///两个刚体是否会发生碰撞
          bool collideConnected;
        };

除了上面的设置,关节还有3个通用的概念。

❑ 锚点:与Cocos2d-x的锚点概念很相似,是运用于旋转移动计算的一个点,如图9-7所示。

图9-7 锚点

❑ 限制:每个关节都有其限制条件,如旋转角度、移动距离等(车轮和轮杆可以进行360°的旋转,却不能移动,人的大腿和小腿大约只能允许180°以内的旋转,非正常情况不算在内)。

❑ 马达:用来描述关节运动,并在物理模拟中制造关节的运动,可以是旋转或者移动。

得到一个b2Joint对象之后,可以做哪些操作呢?可以获取关节的信息,可以控制整个关节,如重新描述它的马达,更新限制等。

9.5.2 旋转关节RevoluteJoint

图9-8 旋转关节

如图9-8所示,旋转关节就像是一个点,把两个刚体联系起来,并且限制它们只能绕着这个点旋转。这个点也可以是在刚体形状以外的一个点。例如,电风扇的叶子都可以围绕着中心的公共点旋转。

使用旋转关节的步骤如下,首先填充一个b2RevoluteJointDef结构体,然后调用World的CreateJoint()函数即可,我们来看一下b2RevoluteJointDef结构体。

        struct b2RevoluteJointDef : public b2JointDef
        {
            ///使用一个世界坐标作为两个刚体的连接点,来初始化关节
            void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor);
            ///A对象的锚点,位于A对象的局部坐标系,是A的旋转点
            b2Vec2 localAnchorA;
            ///B对象的锚点,位于B对象的局部坐标系,是B的旋转点
            b2Vec2 localAnchorB;
            ///参照角度(一个旋转90°的关节,参照角度为0时,返回的旋转是90°,参照角度为
            10°时,返回100°)一般在设置连接器限制时,这个参数会比较有用。b2RevoluteJoint
            的GetJointAngle返回的是两个连接器的相对角度
            float32 referenceAngle;
            ///是否启用限制
            bool enableLimit;
            ///角度最低限制,逆时针方向
            float32 lowerAngle;
            ///角度最高限制,逆时针方向
            float32 upperAngle;
            ///是否启用马达
            bool enableMotor;
            ///马达的目标速度(受限于最大扭力)
            float32 motorSpeed;
            ///马达可以达到的最大扭力
            float32 maxMotorTorque;
        };

下面的代码演示了如何创建一个旋转关节。

        //添加旋转关节
        b2RevoluteJointDef revdef;
        revdef.Initialize(boxbd, circlebd, b2Vec2(0.0f, 10.0f));
        //开启马达
        revdef.enableMotor = true;
        revdef.maxMotorTorque = 105;
        revdef.motorSpeed = 30.14f;
        m_world->CreateJoint(&revdef);

RevoluteJoint的Initialize()函数会使用两个Body的锚点作为旋转锚点,并且根据输入的第3个参数,在世界坐标系中,根据当前两个Body在世界坐标系的位置,作为两个物体的公共顶点。如图9-9和图9-10演示了基于指定公共点的旋转关节运动情况。

图9-9 旋转关节1

图9-10 旋转关节2

关于马达和限制,在启用马达之后,第二个物体会围绕公共点开始旋转,马达会达到motorSpeed的旋转速度,这个旋转的扭力矩不会高于maxMotorTorque。motorSpeed的单位是弧度,而maxMotorTorque的单位是N/m,一个力的单位。给马达赋予一个正的速度,会让其按照逆时针的方向旋转,负的速度会让其顺时针旋转。如图9-11和图9-12演示了马达的开启情况,在创建关节时,这里分别将四边形和圆形作为第二个物体传入。

图9-11 四边形马达

图9-12 圆形马达

启用限制可以限制旋转的角度,可以限制一个超过360°的值,例如3600°,马达在旋转10圈之后,达到限制值会停下来,而手动让其旋转,也无法突破其限制值。

9.5.3 平移关节PrismaticJoint

平移关节可以将两个物体之间的限制在一个特定的方向上平移,就像滑动门和垂直电梯,滑动面只可以向左右两边移动,而垂直电梯一般只能上下移动,平移关节阻止了物体的相对旋转。

        //添加平移关节
        b2PrismaticJointDef pridef;
        pridef.Initialize(boxbd,  circlebd,  b2Vec2(0.0f,  20.0f),  b2Vec2(1.0f,
        0.0f));
        m_world->CreateJoint(&pridef);

b2PrismaticJointDef的Initialize()函数通过传入两个刚体,以及一个公共顶点,还有一个轴来初始化。公共顶点与旋转关节的意义一样,都是世界坐标系下的顶点。传入的轴表示一个方向,用来限制两个刚体的相对移动和旋转,假设传入的轴是(0, 0),那么两个刚体的相对移动不受限制,但两个物体不会产生相对旋转,它还会令马达和限制失效。上面的代码为圆和正方形创建了一个只能在x轴上移动的平移关节,运行效果如图9-13和图9-14所示,两个节点只能沿着相对x轴进行移动。

图9-13 平移关节1

图9-14 平移关节2

9.5.4 距离关节DistanceJoint

距离关节将两个物体之间的距离保持在一定范围内的关节。例如,用弹簧拴住两个物体,也可以像一根棍子一样,让两个物体的距离固定不变,如图9-15所示。

图9-15 距离关节

        //添加距离关节
        b2DistanceJointDef disdef;
        disdef.Initialize(boxbd, circlebd,
            b2Vec2(2.0f, 10.0f), b2Vec2(-2.0f, 10.0f));
        disdef.localAnchorA = b2Vec2(0.0f, 0.0f);
        disdef.localAnchorB = b2Vec2(0.0f, 0.0f);
        //震动频率
        disdef.frequencyHz = 2.0f;
        //阻尼率
        disdef.dampingRatio = 0.0f;
        m_world->CreateJoint(&disdef);

通过设置frequency频率和damping ratio可以获得柔软的效果,frequency表示震荡的频率,单位是Hz,相当于游戏刷新频率的一半,阻尼率的取值一般在0~1之间,震荡幅度从0~1由大变小,如图9-16是使用柔软的距离关节做出的网的效果,可以参考TestBed的Web示例。下面这个例子的frequency取值是2.0而damping ratio取值是0。

图9-16 使用距离关节模拟网

9.5.5 滑轮关节PulleyJoint

滑轮关节可以用来创建一个滑轮的效果,滑轮的两端连接着两个绳子,绳子绑着两个物体,当一个物体上升的时候,另一个物体就下降,如图9-17和图9-18可以很简单清晰地描述出这个效果。

图9-17 滑轮关节1

图9-18 滑轮关节2

        //添加滑轮关节
        b2PulleyJointDef puldef;
        puldef.Initialize(boxbd, circlebd,
        b2Vec2(-2, 15), b2Vec2(2, 15),         //A和B滑轮悬挂点的世界坐标
        b2Vec2(-2, 10), b2Vec2(2, 10),         //A和B滑轮挂载点的世界坐标
            0.1f);                              //滑轮线的阻尼率
        m_world->CreateJoint(&puldef);

9.5.6 鼠标关节MouseJoint

鼠标关节主要用于方便我们用鼠标来拖动物体,它会先确定物体上的一个点,对这个点施加力,使其向鼠标的位置移动。被拖曳的物体可以自由旋转,可以为鼠标关节设置最大力矩来决定力的大小,设置频率和阻尼率,来达到弹簧和减震器的效果。

要实现用鼠标关节来拖曳一个刚体的效果,首先需要先实现单击到刚体的判断,并不是添加了鼠标关节之后,这个刚体就可以被单击了。

因为Box2d并不关注单击事件的实现,点击判断这个功能需要开发者自己来实现,在Box2d里面实现这个功能很简单,可以参考Box2d TestBed里面的Test.cpp文件,里面继承了b2QueryCallback类,用于查询在一个包围盒中的刚体,实现了ReportFixture()方法,在ReportFixture()中,会传入一个b2Fixture,如果判断你的鼠标(或手指)在这个对象的范围内,那么返回false,并保存选中的Fixture,这时候会终止查询,否则返回true。

        class QueryCallback : public b2QueryCallback
        {
        public:
            QueryCallback(const b2Vec2& point)
            {
              m_point = point;
              m_fixture = NULL;
            }

            bool ReportFixture(b2Fixture* fixture)
            {
              b2Body* body = fixture->GetBody();
              if (body->GetType() == b2_dynamicBody)
              {
                  bool inside = fixture->TestPoint(m_point);
                  if (inside)
                  {
                      m_fixture = fixture;
                      //We are done, terminate the query.
                      return false;
                  }
              }

              //Continue the query.
              return true;
            }

            b2Vec2 m_point;
            b2Fixture* m_fixture;
        };

在鼠标按下的时候,需要查询鼠标是否单击到动态刚体,(这里的MouseDown应该由TouchBegin系列函数调用)这时候用到了World的QueryAABB()函数来进行检测,首先在鼠标单击的地方,构造一个非常小的碰撞盒,然后执行查询,这时候位于这个碰撞盒之内的Fixture会被传入到查询对象中,在查询对象中进行一次过滤,只查询动态刚体,并且判断点是否在Fixture之内,这属于边界情况的过滤,刚体位于这个极小的碰撞盒之内,但单击的坐标点并没有落在这个Fixture之内。通过这层层的过滤,在单击到刚体之后,就可以开始创建鼠标关节了。

        void Test::MouseDown(const b2Vec2& p)
        {
            m_mouseWorld = p;

            if (m_mouseJoint ! = NULL)
            {
              return;
            }

            //Make a small box.
            b2AABB aabb;
            b2Vec2 d;
            d.Set(0.001f, 0.001f);
            aabb.lowerBound = p - d;
            aabb.upperBound = p + d;

            //Query the world for overlapping shapes.
            QueryCallback callback(p);
            m_world->QueryAABB(&callback, aabb);

            if (callback.m_fixture)
            {

              b2Body* body = callback.m_fixture->GetBody();
              b2MouseJointDef md;
              md.bodyA = m_groundBody;
              md.bodyB = body;
              md.target = p;
              md.maxForce = 1000.0f * body->GetMass();
              m_mouseJoint = (b2MouseJoint*)m_world->CreateJoint(&md);
              body->SetAwake(true);
            }
        }

要创建一个鼠标关节很简单,将地面(或者其他静态刚体)设置为BodyA,然后将要拖动的刚体设置为BodyB,并设置要移动到的目标点,以及最大力矩,上面代码中是用1000乘以刚体的质量作为最大力矩,还需要将拖动的刚体的状态设置为Awake。

9.5.7 齿轮关节GearJoint

齿轮关节用于模拟现实中的齿轮,可以用一堆形状来描述一个齿轮,然后用旋转关节让这个齿轮旋转,也可以起到这样一个效果,但并不高效,并且在排列齿轮牙齿的时候需要小心翼翼。有了齿轮关节,就可以直接用一个齿轮关节来实现一个齿轮,如图9-19所示。

图9-19 齿轮关节

齿轮关节和其他关节不一样的地方在于,它是一个依赖其他关节的关节。一般的关节只是依赖于关节连接着的两个刚体。通过一个关节的运动,来驱动另外一个关节。如果在删除齿轮关节之前,删除了其他关节,那么程序将会崩溃。在释放的时候,需要先释放齿轮关节,然后再释放挂载在齿轮关节上的其他关节。

下面的代码将创建两个齿轮关节,以及两个旋转关节和一个平移关节,通过齿轮关节的旋转带动其他关节。

        //第一个小圆
        b2CircleShape circle1;
        circle1.m_radius = 1.0f;

        //第二个大圆
        b2CircleShape circle2;
        circle2.m_radius = 2.0f;

        //长方形
        b2PolygonShape box;
        box.SetAsBox(0.5f, 5.0f);

        //小圆刚体
        b2BodyDef bd1;
        bd1.type = b2_dynamicBody;
        bd1.position.Set(-3.0f, 12.0f);
        b2Body* body1 = m_world->CreateBody(&bd1);
        body1->CreateFixture(&circle1, 5.0f);

        //小圆和静态地面用旋转关节连接,小圆只可以旋转,不能移动
        b2RevoluteJointDef jd1;
        jd1.bodyA = ground;
        jd1.bodyB = body1;
        jd1.localAnchorA = ground->GetLocalPoint(bd1.position);
        jd1.localAnchorB = body1->GetLocalPoint(bd1.position);
        jd1.referenceAngle = body1->GetAngle() - ground->GetAngle();
        m_joint1 = (b2RevoluteJoint*)m_world->CreateJoint(&jd1);

        //大圆刚体
        b2BodyDef bd2;
        bd2.type = b2_dynamicBody;
        bd2.position.Set(0.0f, 12.0f);
        b2Body* body2 = m_world->CreateBody(&bd2);
        body2->CreateFixture(&circle2, 5.0f);
        //大圆和静态地面用旋转关节连接
        b2RevoluteJointDef jd2;
        jd2.Initialize(ground, body2, bd2.position);
        m_joint2 = (b2RevoluteJoint*)m_world->CreateJoint(&jd2);

        //长方形刚体
        b2BodyDef bd3;
        bd3.type = b2_dynamicBody;
        bd3.position.Set(2.5f, 12.0f);
        b2Body* body3 = m_world->CreateBody(&bd3);
        body3->CreateFixture(&box, 5.0f);

        //平移关节限制长方形刚体的移动
        b2PrismaticJointDef jd3;
        jd3.Initialize(ground, body3, bd3.position, b2Vec2(0.0f, 1.0f));
        jd3.lowerTranslation = -5.0f;
        jd3.upperTranslation = 5.0f;
        jd3.enableLimit = true;
        m_joint3 = (b2PrismaticJoint*)m_world->CreateJoint(&jd3);

        //连接两个圆形的齿轮关节
        //当一个小圆转动时,另一个小圆会跟着转动
        //小圆连接的关节旋转一周,大圆连接的关节旋转1/2周
        b2GearJointDef jd4;
        jd4.bodyA = body1;
        jd4.bodyB = body2;
        jd4.joint1 = m_joint1;
        jd4.joint2 = m_joint2;
        jd4.ratio = circle2.m_radius / circle1.m_radius;
        m_joint4 = (b2GearJoint*)m_world->CreateJoint(&jd4);

        //连接大圆和长方形的齿轮关节
        //当大圆所在的旋转关节转动时,会驱动平移关节跟着移动
        //平移关节发生移动时,也会驱动旋转关节
        b2GearJointDef jd5;
        jd5.bodyA = body2;
        jd5.bodyB = body3;
        jd5.joint1 = m_joint2;
        jd5.joint2 = m_joint3;
        jd5.ratio = -1.0f / circle2.m_radius;
        m_joint5 = (b2GearJoint*)m_world->CreateJoint(&jd5);

图9-20 齿轮关节

上面的代码运行结果如下,拖动任意一个刚体,会导致关节发生平移运动或者旋转运动,这时运动将会通过齿轮关节,传达到另外一个关节上,按照ratio参数的比例,来赋予另外一个关节运动的能量。在设置齿轮关节的时候,最好先在脑海中想清楚,齿轮动起来是怎样的。齿轮关节的创建本身很简单,上面如此冗长的代码,主要是创建了其他的关节。运行效果如图9-20所示。

9.5.8 滚轮关节WheelJoint

滚轮关节用于模拟汽车的轮子,滚轮关节连接汽车和汽车轮子,汽车轮子可以滚动,滚轮关节还提供了一个弹簧效果,当发生车震的时候,汽车会上下震动,读者可以想像一下一辆越野车在颠簸的路上前进,轮子在转动,轮子和车身的距离不断地放大、缩小,如图9-21所示。

图9-21 滚轮关节

滚轮关节相当于一个旋转关节和一个带弹簧的距离关节组合而成,如图9-22所示的车轮子,当车子被甩起来的时候,轮子会被带出一些,而当车子被压下去的时候,轮子也会跟着陷下去。

图9-22 使用滚轮关节模拟车轮

        b2WheelJointDef jd;
        //滚轮关节两个刚体可以移动的方向是y轴,也就是上下移动
        b2Vec2 axis(0.0f, 1.0f);

        //car表示车身,wheel1和wheel2分别表示车的前后轮子,这是一辆两轮车
        jd.Initialize(m_car, m_wheel1, m_wheel1->GetPosition(), axis);
        //顺时针方向旋转
        jd.motorSpeed = -10.0f;
        //这个轮子的马力更大一些
        jd.maxMotorTorque = 20.0f;
        jd.enableMotor = true;
        jd.frequencyHz = 4.0f;
        jd.dampingRatio = 0.7f;
        m_spring1 = (b2WheelJoint*)m_world->CreateJoint(&jd);

        //第二个轮子
        jd.Initialize(m_car, m_wheel2, m_wheel2->GetPosition(), axis);
        jd.motorSpeed = -10.0f;
        jd.maxMotorTorque = 10.0f;
        jd.enableMotor = false;
        jd.frequencyHz = 4.0f;
        jd.dampingRatio = 0.7f;
        m_spring2 = (b2WheelJoint*)m_world->CreateJoint(&jd);

9.5.9 焊接关节WeldJoint

焊接关节尝试限制两个刚体之间所有的相对运动,但是焊接关节并不是很稳定,效果看起来有些柔软,就像跳水运动员起跳时踩着的跳水板一样。用焊接关节将一大堆动态刚体(小四边形)依次连接起来,在受到力的作用下,摇摇晃晃,就是这种效果,如图9-23所示。

图9-23 焊接关节

上面是由若干小四边形组成的一小块板子,每两个小四边形都用Weld焊接关节连接起来,可以设置dampingRatio来设置它们之间连接在一起的弹性,使用方法非常简单,设置好frequencyHz和dampingRatio,然后传入两个刚体,就可以直接创建焊接关节,Testbed的Cantilever很好地介绍了焊接关节的使用。

        b2PolygonShape shape;
        shape.SetAsBox(0.5f, 0.125f);

        b2FixtureDef fd;
        fd.shape = &shape;
        fd.density = 20.0f;
        //设置弹性

        b2WeldJointDef jd;
        jd.frequencyHz = 8.0f;
        jd.dampingRatio = 0.7f;

        //将所有物体连在一起
        b2Body* prevBody = ground;
        for (int32 i = 0; i < e_count; ++i)
        {
            b2BodyDef bd;
            bd.type = b2_dynamicBody;
            bd.position.Set(5.5f + 1.0f * i, 10.0f);
            b2Body* body = m_world->CreateBody(&bd);
            body->CreateFixture(&fd);

            if (i > 0)
            {
        //创建关节
              b2Vec2 anchor(5.0f + 1.0f * i, 10.0f);
              jd.Initialize(prevBody, body, anchor);
              m_world->CreateJoint(&jd);
            }

            prevBody = body;
        }

9.5.10 摩擦关节FrictionJoint

摩擦关节会制造“从上到下”的摩擦,提供了2D的角摩擦和平移摩擦,单从这句话来看,确实相当费解。想像一下,你拉着一辆车努力地向前冲,会受到一整辆车的摩擦力的影响。想像一下,你在天空中飞速翱翔,速度越快,会感觉到越强大的空气阻力,差不多就是这种感觉了,如图9-24所示。

图9-24 摩擦关节

使用了摩擦关节的小方块们,在被撞击到的时候,会缓缓移动,下面例子的重力加速度设置为0。

        b2FrictionJointDef jd;
        jd.localAnchorA.SetZero();
        jd.localAnchorB.SetZero();
        jd.bodyA = ground;
        jd.bodyB = body;
        jd.collideConnected = true;
        jd.maxForce = mass * gravity;
        jd.maxTorque = mass * radius * gravity;

        m_world->CreateJoint(&jd);

通过设置maxForce来限制刚体移动的阻力,通过设置maxTorque来限制刚体旋转时的阻力,具体可以参考Testbed的ApplyForce示例,Testbed是Box2d官方源码的示例,相当于Box2d的cpp-tests(cpp-test是Cocos2d-x的示例集合,属于Cocos2d-x的常识)。

9.5.11 绳索关节RopeJoint

绳索关节用来模拟绳子,绳子的两端绑着两个刚体,两个刚体可以在绳子限定的范围内自由地移动和旋转,与现实世界使用的绳子没什么两样,这是一根柔软的,拉不断的绳子,如图9-25所示。

图9-25 绳索关节

通过设置b2RopeJointDef的maxLength,可以限定绳子的长度,通过传入两个刚体,可以在这两个刚体身上系一条绳子。

最后,在删除的时候,必须先删除关节,之后才可以删除关节上的物体,否则程序会崩溃。