高級編程技巧只是相對的,其實主要是講物理模擬和着色器程序的使用。
本章主要講解利用Box2D並用它來實現蘿卜雨,然后是使用單色着色器shader讓畫面呈現單色狀態:http://files.cnblogs.com/mignet/shaders.zip
如果你不知道Box2D,那你肯定玩過用它做的游戲:Angry Birds, Limbo, Tiny Wings, Crayon Physics Deluxe
Libgdx集成了Box2D,類似於其他框架,用了很薄的一層java API進行了封裝,等效利用Box2D的所有功能。如果你以前學過Box2D的任何知識,Libgdx這里都可以直接使用。
一些教程:
• C++: http://www.iforce2d.net/b2dtut/
• Objective-C: http://www.raywenderlich.com/28602/intro-to-box2dwith-cocos2d-2-x-tutorial-bouncing-balls
• Flash: http://www.emanueleferonato.com/category/box2d/
• JavaScript: http://blog.sethladd.com/2011/09/box2d-collisiondamage-for-javascript.html
了解Box2D的基本概念:
首先,我們來理解一個看起來很神秘的術語:剛體。
物體,從物理上講,只是一個物質和一些屬性(比如它的位置和方向)的集合,剛體就是不會在外力的作用下變形的理想化的物體。
為簡便起見,剛體(rigid body)簡寫為body。Box2D里提到body其實就是剛體,因為它只支持一種體。
其實Libgdx還集成了另一種物理引擎:Bullet,與Box2D相比,Box2D局限於二維空間和剛體的支持,Bullet支持全面的三維物理模擬以及同時支持剛體和軟體。
我們這里專注Box2D,3D物理是個更高端的話題。哈哈
除了位置和方向,剛體還有:質量(kg)速度(m/s)旋轉速度(rad/s).
剛體類型:
Static:靜態的就是固定的,通常用於地板啊牆啊不動的平台啊等等。不和Static的和Kinematic的物體發生碰撞
Kinematic:可移動的就是位置會變動,可以是手動的或者根據速度變化(首選),通常用於可移動的平台比如電梯,動態物體的鏡像等。不和Static的和Kinematic的物體發生碰撞
Dynamic:動態的就是位置會變動,可以是手動或者是在力的作用下(首選),Dynamic可以和所有其他類型的物體碰撞。通常用於玩家,敵人,道具等等
注意到Kinematic的物體是不受力的作用的,它只會根據自己設定的速度來移動。
使用形狀:
形狀描述的是幾何屬性,比如圓的半徑,矩形的寬和高,或者是一系列的點來描述更復雜的形狀的多邊形,所以,這些形狀定義區域,可以與其他形狀進行碰撞檢測。
使用夾具:
夾具是為形狀增加的材質屬性,比如密度,摩擦力,反彈力。夾具附加到形狀然后附加到body上,所以夾具在body之間的交互上扮演了十分重要的角色。
物理世界:
world是整個物理模型模擬的一個虛擬的沙盒。每個body都需要放在world里。
Box2D是一個非常豐富的引擎,它還包括很多特性,像約束,關節,傳感器,接觸監聽器等等,這里用不到,就不介紹了。
有個物理body編輯器可以試試:https://code.google.com/p/box2d-editor/
它有些很有用的特性:比如可以把凹邊形變成凸多邊形,跟蹤圖像的輪廓,還有個內置的碰撞測試器。
接下來,我們要開始下蘿卜雨了。要添加兩個新的對象到Canyon Bunny。
一個就是關卡結束的點,通關點。另一個就是一個普通胡蘿卜。
當玩家到達通關點,就開始下蘿卜雨。在金色的蘿卜雕像旁,胡蘿卜從天而降,摔倒地上,然后互相堆積。
首先把圖片carrot.png,goal.png添加到images。然后打包到pack。
在Assets修改內部類:
public final AtlasRegion carrot; public final AtlasRegion goal; public AssetLevelDecoration(TextureAtlas atlas) { cloud01 = atlas.findRegion("cloud01"); cloud02 = atlas.findRegion("cloud02"); cloud03 = atlas.findRegion("cloud03"); mountainLeft = atlas.findRegion("mountain_left"); mountainRight = atlas.findRegion("mountain_right"); waterOverlay = atlas.findRegion("water_overlay"); carrot = atlas.findRegion("carrot"); goal = atlas.findRegion("goal"); }
增加Carrot類:
package com.packtpub.libgdx.canyonbunny.game.objects; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.packtpub.libgdx.canyonbunny.game.Assets; import com.packtpub.libgdx.canyonbunny.game.objects.AbstractGameObject; public class Carrot extends AbstractGameObject { private TextureRegion regCarrot; public Carrot() { init(); } private void init() { dimension.set(0.25f, 0.5f); regCarrot = Assets.instance.levelDecoration.carrot; // Set bounding box for collision detection bounds.set(0, 0, dimension.x, dimension.y); origin.set(dimension.x / 2, dimension.y / 2); } public void render(SpriteBatch batch) { TextureRegion reg = null; reg = regCarrot; batch.draw(reg.getTexture(), position.x - origin.x, position.y - origin.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); } }
增加Goal類:
package com.packtpub.libgdx.canyonbunny.game.objects; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.packtpub.libgdx.canyonbunny.game.Assets; public class Goal extends AbstractGameObject { private TextureRegion regGoal; public Goal() { init(); } private void init() { dimension.set(3.0f, 3.0f); regGoal = Assets.instance.levelDecoration.goal; // Set bounding box for collision detection bounds.set(1, Float.MIN_VALUE, 10, Float.MAX_VALUE); origin.set(dimension.x / 2.0f, 0.0f); } public void render(SpriteBatch batch) { TextureRegion reg = null; reg = regGoal; batch.draw(reg.getTexture(), position.x - origin.x, position.y - origin.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); } }
編輯Level圖片,增加一個紅色的色塊表示目的點。然后修改Level類:
package com.packtpub.libgdx.canyonbunny.game; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.utils.Array; import com.packtpub.libgdx.canyonbunny.game.objects.AbstractGameObject; import com.packtpub.libgdx.canyonbunny.game.objects.BunnyHead; import com.packtpub.libgdx.canyonbunny.game.objects.Carrot; import com.packtpub.libgdx.canyonbunny.game.objects.Clouds; import com.packtpub.libgdx.canyonbunny.game.objects.Feather; import com.packtpub.libgdx.canyonbunny.game.objects.Goal; import com.packtpub.libgdx.canyonbunny.game.objects.GoldCoin; import com.packtpub.libgdx.canyonbunny.game.objects.Mountains; import com.packtpub.libgdx.canyonbunny.game.objects.Rock; import com.packtpub.libgdx.canyonbunny.game.objects.WaterOverlay; public class Level { public static final String TAG = Level.class.getName(); public Array<Carrot> carrots; public Goal goal; public enum BLOCK_TYPE { GOAL(255, 0, 0), // red EMPTY(0, 0, 0), // black ROCK(0, 255, 0), // green PLAYER_SPAWNPOINT(255, 255, 255), // white ITEM_FEATHER(255, 0, 255), // purple ITEM_GOLD_COIN(255, 255, 0); // yellow private int color; private BLOCK_TYPE(int r, int g, int b) { color = r << 24 | g << 16 | b << 8 | 0xff; } public boolean sameColor(int color) { return this.color == color; } public int getColor() { return color; } } public BunnyHead bunnyHead; public Array<GoldCoin> goldcoins; public Array<Feather> feathers; // objects public Array<Rock> rocks; // decoration public Clouds clouds; public Mountains mountains; public WaterOverlay waterOverlay; public Level(String filename) { init(filename); } private void init(String filename) { // player character bunnyHead = null; // objects rocks = new Array<Rock>(); goldcoins = new Array<GoldCoin>(); feathers = new Array<Feather>(); carrots = new Array<Carrot>(); // load image file that represents the level data Pixmap pixmap = new Pixmap(Gdx.files.internal(filename)); // scan pixels from top-left to bottom-right int lastPixel = -1; for (int pixelY = 0; pixelY < pixmap.getHeight(); pixelY++) { for (int pixelX = 0; pixelX < pixmap.getWidth(); pixelX++) { AbstractGameObject obj = null; float offsetHeight = 0; // height grows from bottom to top float baseHeight = pixmap.getHeight() - pixelY; // get color of current pixel as 32-bit RGBA value int currentPixel = pixmap.getPixel(pixelX, pixelY); // find matching color value to identify block type at (x,y) // point and create the corresponding game object if there is // a match // empty space if (BLOCK_TYPE.EMPTY.sameColor(currentPixel)) { // do nothing } // rock else if (BLOCK_TYPE.ROCK.sameColor(currentPixel)) { if (lastPixel != currentPixel) { obj = new Rock(); float heightIncreaseFactor = 0.25f; offsetHeight = -2.5f; obj.position.set(pixelX, baseHeight * obj.dimension.y * heightIncreaseFactor + offsetHeight); rocks.add((Rock) obj); } else { rocks.get(rocks.size - 1).increaseLength(1); } } // player spawn point else if (BLOCK_TYPE.PLAYER_SPAWNPOINT.sameColor(currentPixel)) { obj = new BunnyHead(); offsetHeight = -3.0f; obj.position.set(pixelX, baseHeight * obj.dimension.y + offsetHeight); bunnyHead = (BunnyHead) obj; } // feather else if (BLOCK_TYPE.ITEM_FEATHER.sameColor(currentPixel)) { obj = new Feather(); offsetHeight = -1.5f; obj.position.set(pixelX, baseHeight * obj.dimension.y + offsetHeight); feathers.add((Feather) obj); } // gold coin else if (BLOCK_TYPE.ITEM_GOLD_COIN.sameColor(currentPixel)) { obj = new GoldCoin(); offsetHeight = -1.5f; obj.position.set(pixelX, baseHeight * obj.dimension.y + offsetHeight); goldcoins.add((GoldCoin) obj); } // goal else if (BLOCK_TYPE.GOAL.sameColor(currentPixel)) { obj = new Goal(); offsetHeight = -7.0f; obj.position.set(pixelX, baseHeight + offsetHeight); goal = (Goal) obj; } // unknown object/pixel color else { int r = 0xff & (currentPixel >>> 24); // red color channel int g = 0xff & (currentPixel >>> 16); // green color channel int b = 0xff & (currentPixel >>> 8); // blue color channel int a = 0xff & currentPixel; // alpha channel Gdx.app.error(TAG, "Unknown object at x<" + pixelX + "> y<" + pixelY + ">: r<" + r + "> g<" + g + "> b<" + b + "> a<" + a + ">"); } lastPixel = currentPixel; } } // decoration clouds = new Clouds(pixmap.getWidth()); clouds.position.set(0, 2); mountains = new Mountains(pixmap.getWidth()); mountains.position.set(-1, -1); waterOverlay = new WaterOverlay(pixmap.getWidth()); waterOverlay.position.set(0, -3.75f); // free memory pixmap.dispose(); Gdx.app.debug(TAG, "level '" + filename + "' loaded"); } public void update(float deltaTime) { bunnyHead.update(deltaTime); for (Rock rock : rocks) rock.update(deltaTime); for (GoldCoin goldCoin : goldcoins) goldCoin.update(deltaTime); for (Feather feather : feathers) feather.update(deltaTime); for (Carrot carrot : carrots) carrot.update(deltaTime); clouds.update(deltaTime); } public void render(SpriteBatch batch) { // Draw Mountains mountains.render(batch); // Draw Goal goal.render(batch); // Draw Rocks for (Rock rock : rocks) rock.render(batch); // Draw Gold Coins for (GoldCoin goldCoin : goldcoins) goldCoin.render(batch); // Draw Feathers for (Feather feather : feathers) feather.render(batch); // Draw Carrots for (Carrot carrot : carrots) carrot.render(batch); // Draw Player Character bunnyHead.render(batch); // Draw Water Overlay waterOverlay.render(batch); // Draw Clouds clouds.render(batch); } }
現在,我們修改AbstractGameObject讓它下蘿卜雨:
public Body body; public void update(float deltaTime) { if (body == null) { updateMotionX(deltaTime); updateMotionY(deltaTime); // Move to new position position.x += velocity.x * deltaTime; position.y += velocity.y * deltaTime; } else { position.set(body.getPosition()); rotation = body.getAngle() * MathUtils.radiansToDegrees; } }
這里的意思就是說如果我們的物體對象不是body的話,那就按照我們自己的簡易物理模擬來移動,如果是的話,就通過Box2D計算反饋的值來移動。
接下來在WorldController添加:
private boolean goalReached; public World b2world; private void initPhysics() { if (b2world != null) b2world.dispose(); b2world = new World(new Vector2(0, -9.81f), true); // Rocks Vector2 origin = new Vector2(); for (Rock rock : level.rocks) { BodyDef bodyDef = new BodyDef(); bodyDef.type = BodyType.KinematicBody; bodyDef.position.set(rock.position); Body body = b2world.createBody(bodyDef); rock.body = body; PolygonShape polygonShape = new PolygonShape(); origin.x = rock.bounds.width / 2.0f; origin.y = rock.bounds.height / 2.0f; polygonShape.setAsBox(rock.bounds.width / 2.0f, rock.bounds.height / 2.0f, origin, 0); FixtureDef fixtureDef = new FixtureDef(); fixtureDef.shape = polygonShape; body.createFixture(fixtureDef); polygonShape.dispose(); } }
一直記得在world不用的時候釋放掉,當然也包括PolygonShape, CircleShape這些Box2D的shape類
body物體都會有藍色的邊框,這是Box2D的類Box2DDebugRenderer的效果。我們在WorldRenderer里加上它:
private static final boolean DEBUG_DRAW_BOX2D_WORLD = false; private Box2DDebugRenderer b2debugRenderer; private void init () { batch = new SpriteBatch(); camera = new OrthographicCamera(Constants.VIEWPORT_WIDTH, Constants.VIEWPORT_HEIGHT); camera.position.set(0, 0, 0); camera.update(); cameraGUI = new OrthographicCamera(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT); cameraGUI.position.set(0, 0, 0); cameraGUI.setToOrtho(true); // flip y-axis cameraGUI.update(); b2debugRenderer = new Box2DDebugRenderer(); } private void renderWorld (SpriteBatch batch) { worldController.cameraHelper.applyTo(camera); batch.setProjectionMatrix(camera.combined); batch.begin(); worldController.level.render(batch); batch.end(); if (DEBUG_DRAW_BOX2D_WORLD) { b2debugRenderer.render(worldController.b2world, camera.combined); } }
增加一些常量准備:
// Number of carrots to spawn public static final int CARROTS_SPAWN_MAX = 100; // Spawn radius for carrots public static final float CARROTS_SPAWN_RADIUS = 3.5f; // Delay after game finished public static final float TIME_DELAY_GAME_FINISHED = 6;
在WorldController添加產生蘿卜的代碼:
private void spawnCarrots(Vector2 pos, int numCarrots, float radius) { float carrotShapeScale = 0.5f; // create carrots with box2d body and fixture for (int i = 0; i < numCarrots; i++) { Carrot carrot = new Carrot(); // calculate random spawn position, rotation, and scale float x = MathUtils.random(-radius, radius); float y = MathUtils.random(5.0f, 15.0f); float rotation = MathUtils.random(0.0f, 360.0f) * MathUtils.degreesToRadians; float carrotScale = MathUtils.random(0.5f, 1.5f); carrot.scale.set(carrotScale, carrotScale); // create box2d body for carrot with start position // and angle of rotation BodyDef bodyDef = new BodyDef(); bodyDef.position.set(pos); bodyDef.position.add(x, y); bodyDef.angle = rotation; Body body = b2world.createBody(bodyDef); body.setType(BodyType.DynamicBody); carrot.body = body; // create rectangular shape for carrot to allow // interactions (collisions) with other objects PolygonShape polygonShape = new PolygonShape(); float halfWidth = carrot.bounds.width / 2.0f * carrotScale; float halfHeight = carrot.bounds.height / 2.0f * carrotScale; polygonShape.setAsBox(halfWidth * carrotShapeScale, halfHeight * carrotShapeScale); // set physics attributes FixtureDef fixtureDef = new FixtureDef(); fixtureDef.shape = polygonShape; fixtureDef.density = 50; fixtureDef.restitution = 0.5f; fixtureDef.friction = 0.5f; body.createFixture(fixtureDef); polygonShape.dispose(); // finally, add new carrot to list for updating/rendering level.carrots.add(carrot); } }
private void onCollisionBunnyWithGoal() { goalReached = true; timeLeftGameOverDelay = Constants.TIME_DELAY_GAME_FINISHED; Vector2 centerPosBunnyHead = new Vector2(level.bunnyHead.position); centerPosBunnyHead.x += level.bunnyHead.bounds.width; spawnCarrots(centerPosBunnyHead, Constants.CARROTS_SPAWN_MAX, Constants.CARROTS_SPAWN_RADIUS); } private void initLevel() { score = 0; scoreVisual = score; goalReached = false; level = new Level(Constants.LEVEL_01); cameraHelper.setTarget(level.bunnyHead); initPhysics(); } private void testCollisions (float deltaTIme) { r1.set(level.bunnyHead.position.x, level.bunnyHead.position.y, level.bunnyHead.bounds.width, level.bunnyHead.bounds.height); // Test collision: Bunny Head <-> Rocks ... // Test collision: Bunny Head <-> Gold Coins ... // Test collision: Bunny Head <-> Feathers ... // Test collision: Bunny Head <-> Goal if (!goalReached) { r2.set(level.goal.bounds); r2.x += level.goal.position.x; r2.y += level.goal.position.y; if (r1.overlaps(r2)) onCollisionBunnyWithGoal(); } } public void update (float deltaTime) { handleDebugInput(deltaTime); if (isGameOver() || goalReached) { timeLeftGameOverDelay -= deltaTime; if (timeLeftGameOverDelay < 0) backToMenu(); } else { handleInputGame(deltaTime); } level.update(deltaTime); testCollisions(); b2world.step(deltaTime, 8, 3); cameraHelper.update(deltaTime); }
最后修改Rock:
@Override public void update(float deltaTime) { super.update(deltaTime); floatCycleTimeLeft -= deltaTime; if (floatTargetPosition == null) floatTargetPosition = new Vector2(position); if (floatCycleTimeLeft <= 0) { floatCycleTimeLeft = FLOAT_CYCLE_TIME; floatingDownwards = !floatingDownwards; body.setLinearVelocity(0, FLOAT_AMPLITUDE* (floatingDownwards ? -1 : 1)); } else { body.setLinearVelocity(body.getLinearVelocity().scl(0.98f)); } /*floatTargetPosition.y += FLOAT_AMPLITUDE * (floatingDownwards ? -1 : 1); } position.lerp(floatTargetPosition, deltaTime);*/ }
有人發現了上面提到的問題沒?上面明明說world要dispose,但是又沒釋放,這不是自相矛盾嗎?下面我們來釋放它,首先要讓worldcontroller實現dispose接口:implements Disposable
@Override public void dispose () { if (b2world != null) b2world.dispose(); }
然后修改GameScreen的hide方法:
worldController.dispose();
Ok,大功告成,跑起。。
現在,讓我們把注意力放到着色器shader上來。
這也是只有OpenGL (ES) 2.0支持的功能。它就是利用叫做可編程管線的東東。着色器通常是小程序,它允許我們接管控制圖形處理器渲染場景的某些階段。因此,着色器在今天的計算機圖形學領域是一個重要的組成部分,也是一個用來創建各種各樣的(特殊)的其他方式很難實現的效果的極其強大的工具。為了簡單起見,我們在這里將只討論頂點和片段着色器(vertex and fragment)。
(片段着色器也稱為像素着色器,不幸的是,這個名字有點誤導,這種類型的着色器實際上操作的是片段而不是像素)
以下原因將告訴你為什么着色器通常是有用的以及為什么強烈建議每個(圖形)的程序員把它作為必備列入工具箱
•可編程GPU的渲染管道通過着色器來創建任意復雜的效果。這意味着通過數學公式表述特效的高度靈活性。
•着色器是運行在GPU上的,這會節省CPU時間,讓它可以把時間花在其他的任務上,比如做物理和一般游戲邏輯。
•重度的數學計算在gpu上通常比cpu更快的完成。
•GPU可以並行處理頂點和片段。
每個頂點的頂點着色器的操作給了GPU,一個頂點是一個在3d空間帶屬性(如位置,顏色和紋理坐標)的點.着色器就可以操縱這些值來達到效果,比如一個對象的變形。
通過頂點着色器計算的每個頂點的輸出傳輸到渲染管道作為下一個階段渲染的輸入。
片段着色器計算每個片段像素的顏色,這樣,很多因素都可以控制來渲染不同的材質,這些因素包含光照lighting, 透明translucency, 陰影shadows等等。
一個頂點和片段着色器的組合被稱為一個着色器程序。
着色器通常是寫在一個特定api的高級語言里,比如OpenGL Shading Language (GLSL) for OpenGL。語法類似於C。更多信息請google。
創建一個單色過濾着色器程序
我們先從vertex shader開始
在CanyonBunnyandroid/assets下創建一個子文件夾shaders。然后在里面新建一個文件monochrome.vs。
在里面添加代碼:
attribute vec4 a_position; attribute vec4 a_color; attribute vec2 a_texCoord0; varying vec4 v_color; varying vec2 v_texCoords; uniform mat4 u_projTrans; void main() { v_color = a_color; v_texCoords = a_texCoord0; gl_Position = u_projTrans * a_position; }
前6行聲明的不同的變量用GLSL的術語叫做存儲限定符。
接下來創建文件monochrome.fs,添加代碼:
#ifdef GL_ES precision mediump float; #endif varying vec4 v_color; varying vec2 v_texCoords; uniform sampler2D u_texture; uniform float u_amount; void main() { vec4 color = v_color * texture2D(u_texture, v_texCoords); float grayscale = dot(color.rgb, vec3(0.222, 0.707, 0.071)); color.rgb = mix(color.rgb, vec3(grayscale), u_amount); gl_FragColor = color; }
接下來,在我們的游戲中使用單色過濾着色器程序。
先在常量里添加
// Shader public static final String shaderMonochromeVertex = "shaders/monochrome.vs"; public static final String shaderMonochromeFragment = "shaders/monochrome.fs";
修改GamePreferences的load和save:
public boolean useMonochromeShader; public void load () { showFpsCounter = prefs.getBoolean("showFpsCounter", false); useMonochromeShader = prefs.getBoolean("useMonochromeShader", false); } public void save () { prefs.putBoolean("showFpsCounter", showFpsCounter); prefs.putBoolean("useMonochromeShader", useMonochromeShader); prefs.flush(); }
修改MenuScreen:
private CheckBox chkUseMonochromeShader; private Table buildOptWinDebug () { Table tbl = new Table(); // + Title: "Debug" // + Checkbox, "Show FPS Counter" label // + Checkbox, "Use Monochrome Shader" label chkUseMonochromeShader = new CheckBox("", skinLibgdx); tbl.add(new Label("Use Monochrome Shader", skinLibgdx)); tbl.add(chkUseMonochromeShader); tbl.row(); return tbl; } private void loadSettings () { chkShowFpsCounter.setChecked(prefs.showFpsCounter); chkUseMonochromeShader.setChecked(prefs.useMonochromeShader); } private void saveSettings () { prefs.showFpsCounter = chkShowFpsCounter.isChecked(); prefs.useMonochromeShader = chkUseMonochromeShader.isChecked(); prefs.save(); }
修改WorldRenderer:
private ShaderProgram shaderMonochrome; private void init () { batch = new SpriteBatch(); camera = new OrthographicCamera(Constants.VIEWPORT_WIDTH, Constants.VIEWPORT_HEIGHT); camera.position.set(0, 0, 0); camera.update(); cameraGUI = new OrthographicCamera(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT); cameraGUI.position.set(0, 0, 0); cameraGUI.setToOrtho(true); // flip y-axis cameraGUI.update(); b2debugRenderer = new Box2DDebugRenderer(); shaderMonochrome = new ShaderProgram( Gdx.files.internal(Constants.shaderMonochromeVertex), Gdx.files.internal(Constants.shaderMonochromeFragment)); if (!shaderMonochrome.isCompiled()) { String msg = "Could not compile shader program: " + shaderMonochrome.getLog(); throw new GdxRuntimeException(msg); } } private void renderWorld (SpriteBatch batch) { worldController.cameraHelper.applyTo(camera); batch.setProjectionMatrix(camera.combined); batch.begin(); if (GamePreferences.instance.useMonochromeShader) { batch.setShader(shaderMonochrome); shaderMonochrome.setUniformf("u_amount", 1.0f); } worldController.level.render(batch); batch.setShader(null); batch.end(); if (DEBUG_DRAW_BOX2D_WORLD) { b2debugRenderer.render(worldController.b2world, camera.combined); } } @Override public void dispose () { batch.dispose(); shaderMonochrome.dispose(); }
跑起..
速度計的使用:
在Libgdx中,通常的用法float ax = Gdx.input.getAccelerometerX();
一個取自Android SDK開發網站的圖像很好地說明了傳感器坐標系統:
直接上代碼,首先在常量類中增加:
// Angle of rotation for dead zone (no movement) public static final float ACCEL_ANGLE_DEAD_ZONE = 5.0f; // Max angle of rotation needed to gain max movement velocity public static final float ACCEL_MAX_ANGLE_MAX_MOVEMENT = 20.0f;
然后修改WorldController:
private boolean accelerometerAvailable; private void init () { accelerometerAvailable = Gdx.input.isPeripheralAvailable( Peripheral.Accelerometer); cameraHelper = new CameraHelper(); lives = Constants.LIVES_START; livesVisual = lives; timeLeftGameOverDelay = 0; initLevel(); } private void handleInputGame (float deltaTime) { if (cameraHelper.hasTarget(level.bunnyHead)) { // Player Movement if (Gdx.input.isKeyPressed(Keys.LEFT)) { ... } else { // Use accelerometer for movement if available if (accelerometerAvailable) { // normalize accelerometer values from [-10, 10] to [-1, 1] // which translate to rotations of [-90, 90] degrees float amount = Gdx.input.getAccelerometerY() / 10.0f; amount *= 90.0f; // is angle of rotation inside dead zone? if (Math.abs(amount) < Constants.ACCEL_ANGLE_DEAD_ZONE) { amount = 0; } else { // use the defined max angle of rotation instead of // the full 90 degrees for maximum velocity amount /= Constants.ACCEL_MAX_ANGLE_MAX_MOVEMENT; } level.bunnyHead.velocity.x = level.bunnyHead.terminalVelocity.x * amount; } // Execute auto-forward movement on non-desktop platform else if (Gdx.app.getType() != ApplicationType.Desktop) { level.bunnyHead.velocity.x = level.bunnyHead.terminalVelocity.x; } } } }
效果自己試試吧,本章到此完。
下一章將介紹Libgdx的動作和動畫