- 精通Cocos2d-x游戲開發(進階卷)
- 王永寶
- 4994字
- 2020-11-28 22:37:07
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,可以限定繩子的長度,通過傳入兩個剛體,可以在這兩個剛體身上系一條繩子。
最后,在刪除的時候,必須先刪除關節,之后才可以刪除關節上的物體,否則程序會崩潰。