2.Libgdx擴充套件學習之Box2D_剛體和形狀
文章中涉及的很多概念,都是來自《Box2D中文手冊》。有統一的解釋方便理解。
物體(剛體/Body)
概念介紹
物體具有位置和速度。可以將力(forces)、扭矩(torques)、衝量(impulses)應用到物體上。 物體可以是靜態的(static)、運動但不受力的(kinematic)或動態的(dynamic)。這是物體的型別定義:
1.Static Body
static 物體在模擬時不會運動,就好像它具有無窮大的質量。在 Box2D 內部,會將 static 物體的質量和質量的倒數儲存為零。 static 物體可以讓使用者手動移動。它的速度為零,另外也不會和其它static 或 kinematic 物體相互碰撞。
2. Kinematic Body
kinematic 物體在模擬時以一定的速度運動,但不受力的作用。它們可以讓使用者手動移動,但通常的做法是設定一定的速度來移動它。 kinematic 物體的行為表現就好像它具有無窮大的質量,Box2D 將它的質量和質量的倒數儲存為零。
3. Dynamic Body
dynamic 物體被完全模擬。它們可以讓使用者手動移動,但通常它們都是受力的作用而運動。dynamic 物體可以和其它所有型別的物體相互碰撞。 dynamic 物體的質量總是有限大的,非零的。如果你試圖將它的質量設定為零,它會自動地將質量修改成一千克,並且它不會轉動。
物體定義
在建立物體之前你需要先建立物體定義(BodyDef)。物體定義含有建立並初始化物體所需的資料。Box2D 會從物體定義中複製資料,並不會儲存它的指標。這意味著你可以重複使用同一個物體定義去建立多個物體。
Libgdx也提供給我們一個Java版本的BodyDef,現在可以先簡單瀏覽一下程式碼:
public class BodyDef {
/** The body type. static: zero mass, zero velocity, may be manually moved kinematic: zero mass, non-zero velocity set by user,
* moved by solver dynamic: positive mass, non-zero velocity determined by forces, moved by solver */
public enum BodyType {
StaticBody(0), KinematicBody(1), DynamicBody(2);
private int value;
private BodyType (int value) {
this.value = value;
}
public int getValue () {
return value;
}
};
/** The body type: static, kinematic, or dynamic. Note: if a dynamic body would have zero mass, the mass is set to one. **/
public BodyType type = BodyType.StaticBody;
/** The world position of the body. Avoid creating bodies at the origin since this can lead to many overlapping shapes. **/
public final Vector2 position = new Vector2();
/** The world angle of the body in radians. **/
public float angle = 0;
/** The linear velocity of the body's origin in world co-ordinates. **/
public final Vector2 linearVelocity = new Vector2();
/** The angular velocity of the body. **/
public float angularVelocity = 0;
/** Linear damping is use to reduce the linear velocity. The damping parameter can be larger than 1.0f but the damping effect
* becomes sensitive to the time step when the damping parameter is large. **/
public float linearDamping = 0;
/** Angular damping is use to reduce the angular velocity. The damping parameter can be larger than 1.0f but the damping effect
* becomes sensitive to the time step when the damping parameter is large. **/
public float angularDamping = 0;
/** Set this flag to false if this body should never fall asleep. Note that this increases CPU usage. **/
public boolean allowSleep = true;
/** Is this body initially awake or sleeping? **/
public boolean awake = true;
/** Should this body be prevented from rotating? Useful for characters. **/
public boolean fixedRotation = false;
/** Is this a fast moving body that should be prevented from tunneling through other moving bodies? Note that all bodies are
* prevented from tunneling through kinematic and static bodies. This setting is only considered on dynamic bodies.
* @warning You should use this flag sparingly since it increases processing time. **/
public boolean bullet = false;
/** Does this body start out active? **/
public boolean active = true;
/** Scale the gravity applied to this body. **/
public float gravityScale = 1;
}
物體型別
前面介紹過,物體有3中型別: static、kinematic、和dynamic。在建立時就應該確定號物體型別,否則以後修改,代價很高
bodyDef.type = BodyType.Dynamic
位置和角度
物體定義提供了一個在建立時初始化位置的機會。 這比在World原點建立物體後再移動到某個位置更高效。
不要在原點建立物體後再移動它。如果在原點上同時建立了幾個物體,效能會很差。
阻尼
阻尼用於減小物體在世界中的速度。阻尼跟摩擦有所不同,摩擦僅在物體有接觸的時候才會發生。阻尼並不能取代摩擦,往往這兩個效果需要同時使用。
阻尼引數的範圍可以在 0 到無窮大之間, 0 表示沒有阻尼,無窮大表示滿阻尼。通常來說,阻尼的值應 該在 0 到 0.1 之間。通常我不使用線性阻尼, 因為它會使物體看起來有點漂浮。
bodyDef.linearDamping = 0.0f;
bodyDef.angularDamping = 0.01f;
阻尼類似穩定性與效能, 在值較小的時候阻尼效應幾乎不依賴於時間步,值較大的時候阻尼效應將隨著時間步而變化。如果你使用固定的時間步(推薦)這就不是問題了。
休眠引數
當 Box2D 確定一個物體(或一組物體)已停止移動時,物體就會進入休眠狀態。休眠物體只消耗很小的 CPU 開銷。如果一個醒著的物體接觸到了一個休眠中的物體,那麼休眠中的物體就會醒過來。當物體上的關節或觸點被摧毀的時候,它們同樣會醒過來。你也可以手動地喚醒物體。
通過物體定義,你可以指定一個物體是否可以休眠,或者建立一個休眠的物體。
bodyDef.allowSleep = true;
bodyDef.awake = true;
固定旋轉
想讓一個剛體,比如某個角色,具有固定的旋轉角。這樣物體即使在負載下,也不會旋轉。你可以設定 fixedRotation 來達到這個目的:
bodyDef.fixedRotation = true;
固定旋轉標記使得轉動慣量和它的倒數被設定成零。
子彈
遊戲模擬通常以一定幀率(frame rate)產生一系列的圖片。這就是所謂的離散模擬。在離散模擬中,在一個時間步內剛體可能移動較大距離。如果一個物理引擎沒有處理好大幅度的運動,你就可能會看見一些物體錯誤地穿過了彼此。這被稱為隧穿效應(tunneling)。
預設情況下, Box2D 會通過連續碰撞檢測(CCD)來防止動態物體穿越靜態物體。因此在 Box2D 中,高速移動的物體可以標記成子彈(bullet)。子彈跟 static 或者 dynamic 物體之間都會執行 CCD。你需要按照遊戲的設計來決定哪些物體是子彈。如果你決定一個物體應該按照子彈去處理,可使用下面的設定。子彈標記隻影響 dynamic 物體。
bodyDef.bullet = true;
活動狀態
你可能希望建立一個物體並不參與碰撞和動態模擬。這狀態跟休眠有點類似,但並不會被其它物體喚醒,它上面的 fixture 也不會 被放到 broad-phase 中。也就是說,物體不會參於碰撞檢測,光線投射(ray casts)等等。你可以建立一個非活動的物體,之後再啟用它。
bodyDef.active = true;
關節也可以連線到非活動的物體。但這些關節並不會被模擬。你要小心,當啟用物體時,它的關節不會被扭曲(distorted)。
注意,啟用一個物體和重新建立一個物體的開銷差不多。因此你不應該在流世界( streaming worlds)中使用啟用,而應該用建立和銷燬來節省記憶體。
(譯註: streaming worlds 是指該世界中的大多數物體是動態建立的,而不是一開始就有的。 )
重力因子
可以使用重力因子來調整單個物體上的重力。這需要足夠的細心,增加的重力會降低穩定性。
bodyDef.gravityScale = 0.0f;
使用物體
建立完一個物體之後,你可以對它進行許多操作。其中包括設定質量屬性,訪問其位置和速度,施加力,以及轉換點和向量,即BodyDef裡面定義的資料都可以後期更改。
質量資料
每個物體都有質量(標量)、質心(二維向量)和轉動慣性(標量)。對於 static 物體,它的質量和轉動慣性都被設為零。當物體設定成固定旋轉(fixed rotation),它的轉動慣性也是零。
public class MassData {
/** The mass of the shape, usually in kilograms. **/
public float mass; // 質量(標量)
/** The position of the shape's centroid relative to the shape's origin. **/
public final Vector2 center = new Vector2(); // 質心(二維向量)
/** The rotational inertia of the shape about the local origin. **/
public float I; // 轉動慣性(標量)
}
通常情況下,當 fixture 新增到物體上時,物體的質量屬性會自動地確定。你也可以在執行時(runtime)調整物體的質量。當你有特殊的遊戲方案需要改變質量時,可以這樣做。
setMassData (MassData data)
形狀(碰撞檢測)
形狀描述了可相互碰撞的幾何物件,它的使用獨立於物理模擬。知道如何建立shape,並將之附加到剛體上。
Shape 是個基類, Box2D 的各種形狀都實現了這個基類。此基類定義了幾個函式:
- 判斷一個點與形狀是否有重疊(碰撞)
- 在形狀上執行光線投射(ray cast)
- 計算形狀的AABB
計算形狀的質量
每個形狀都有成員變數:型別(type)和半徑(radius)。對於多邊形,半徑也是有意義的。注意 shape並不知道body,也與力學系統無關。Shape 採用一種緊湊格式來進行儲存,這種格式經過尺寸和效能的優化。因此, shape 並不方便移動,你必須通過手動的設定形狀頂點來移動 shape。然而,當使用 fixture 將 shape 新增到 body 上之後, shape 就會和他的宿主 body 一起移動。
當一個shape沒有新增到body上時,它的頂點用世界座標系來表示。
- 當一個shape新增到body上時,它的頂點用區域性座標系來表示。
// Body位置為(1, 1), 多邊形和Body繫結在一起,那邊座標都要+1
BodyDef bodyDef = new BodyDef();
bodyDef.position.set(1, 1);
Body body1 = world.createBody(bodyDef);
PolygonShape polygonShape = new PolygonShape();
Vector2[] vector2s = new Vector2[4];
vector2s[0] = new Vector2(0, 0);
vector2s[1] = new Vector2(0.0f, 1.0f);
vector2s[2] = new Vector2(1.0f, 1.0f);
vector2s[3] = new Vector2(1.0f, 0.0f);
polygonShape.set(vector2s);
body1.createFixture(polygonShape, 0);
圓形
圓形有位置和半徑。圓形是實心的,沒有辦法使圓形變成空心。
CircleShape circleShape = new CircleShape();
circleShape.setRadius(0.5f);
circleShape.setPosition(new Vector2(3.0f, 3.0f));
多邊形
Box2D 的多邊形是實心的凸(Convex)多邊形。多邊形是實心的,而不是空心的。一個多邊形必須有 3 個或以上的頂點。多邊形的頂點以逆時針( counter clockwise winding, CCW)的順序儲存。
Vector2 vetices[3]
vertices[0] = new Vector2(0.0f, 0.0f)
vertices[1] = new Vector2(1.0f, 0.0f)
vertices[2] = new Vector2(0.0f, 1.0f)
PolygonShape polygon = new PolygonShape()
polygon.set(vetices)
另外,多邊形有一些方便的方法來建立box
public void setAsBox (float hx, float hy)
邊框形狀(EdgeShape)
邊框形狀由一些線段組成。可輔助為你的遊戲建立一個形狀自由的靜態環境。邊框形狀的主要限制在於它們能夠與圓形和多邊形碰撞,但它們之間卻不會碰撞。 Box2D 使用的碰撞演算法要求兩個碰撞物體中至少有一個有體積。邊框形狀沒有體積。所以邊框形狀之間的碰撞是不可能的。
EdgeShape edgeShape = new EdgeShape();
edgeShape.set(5.4f, 3.0f, 7.3f, 2.0f);
連結形狀(ChainShape)
連結形狀提供了一種有效的方式,來將許多邊框連線在一起,用以構建你的靜態遊戲世界。連結形狀自動消除幽靈碰撞,並提供兩側的碰撞。
/**
* 主要介紹剛體(rigid body)和形狀(shap)
*
* 剛體主要有3類 1. Static Body 靜態剛體.零質量,沒有運動,不接受反作用力等,可以人為移動
* 2. Kinematic Body 運動剛體,零質量,求解器(solver)控制速度
* 3. Dynamic Body 動態剛體,有質量,速度受力影響,求解器控制速度
*
* Shape 主要用作碰撞檢測,可以在任何時候建立。不再使用時要銷燬 dispose(),比如可以在呼叫body.createFixture() 之後銷燬
* 1. Circle 圓形
* 2. Edge
* 3. Polygon 多邊形
* 4. Chain 鍊形
*/
public class BodyShape extends ApplicationAdapter {
World world;
Box2DDebugRenderer box2DDebugRenderer;
Body rectBody, circleBody, chainBody, edgeBody;
OrthographicCamera camera;
float scene_width = 12.8f;
float scene_height = 7.2f;
@Override
public void create() {
world = new World(new Vector2(0.0f, -9.8f), true);
box2DDebugRenderer = new Box2DDebugRenderer();
BodyDef bodyDef = new BodyDef();
// 預設是Static Body,只有定義Dynamic 才會受重力影響
bodyDef.type = BodyDef.BodyType.DynamicBody;
bodyDef.position.set(1f, 3f);
// bodyDef.angularVelocity = 1.0f;
rectBody = world.createBody(bodyDef);
circleBody = world.createBody(bodyDef);
chainBody = world.createBody(bodyDef);
edgeBody = world.createBody(bodyDef);
// 正方形
PolygonShape polygonShape = new PolygonShape();
polygonShape.setAsBox(0.6f, 0.3f);
Gdx.app.log("BodyShape", "VertexCount = " + polygonShape.getVertexCount());
FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = polygonShape;
fixtureDef.density = 2;
fixtureDef.restitution = 1.0f;
rectBody.createFixture(fixtureDef);
polygonShape.dispose();
CircleShape circleShape = new CircleShape();
circleShape.setRadius(0.5f);
circleShape.setPosition(new Vector2(3.0f, 3.0f));
fixtureDef.shape = circleShape;
fixtureDef.isSensor = false;
circleBody.createFixture(fixtureDef);
circleShape.dispose();
EdgeShape edgeShape = new EdgeShape();
edgeShape.set(5.4f, 3.0f, 7.3f, 2.0f);
fixtureDef.shape = edgeShape;
edgeBody.createFixture(fixtureDef);
edgeShape.dispose();
ChainShape chainShape = new ChainShape();
float[] ver = {8.0f, 2, 8.9f, 3, 10.4f, 3, 11.4f, 4};
chainShape.createChain(ver);
// chainShape.createLoop(ver);
fixtureDef.shape = chainShape;
chainBody.createFixture(fixtureDef);
chainShape.dispose();
camera = new OrthographicCamera(scene_width, scene_height);
camera.position.set(scene_width / 2, scene_height / 2, 0);
camera.update();
/** 建立測試地面盒子 **/
createGround();
}
@Override
public void dispose() {
world.dispose();
box2DDebugRenderer.dispose();
}
@Override
public void render() {
world.step( 1/ 60f, 6, 2);
Gdx.gl.glClearColor(0.39f, 0.58f, 0.92f, 1.0f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
box2DDebugRenderer.render(world, camera.combined);
}
private void createGround() {
BodyDef goundBodyDef = new BodyDef();
goundBodyDef.position.set(scene_width * 0.5f, 0.5f);
Body groundBody = world.createBody(goundBodyDef);
PolygonShape groundBox = new PolygonShape();
groundBox.setAsBox(scene_width * 0.5f, 0.5f);
groundBody.createFixture(groundBox, 0);
groundBox.dispose();
}
}