[libgdx游戏开发教程]使用Libgdx进行游戏开发(6)-添加主角和道具
如前所述,我们的主角是兔子头。接下来我们实现它。
首先对AbstractGameObject添加变量并初始化:
public Vector2 velocity; public Vector2 terminalVelocity; public Vector2 friction; public Vector2 acceleration; public Rectangle bounds;
分别是速度,极限速度,摩擦力,加速度和边界。
然后我们需要加点物理仿真:
protected void updateMotionX(float deltaTime) { if (velocity.x != 0) { // Apply friction if (velocity.x > 0) { velocity.x = Math.max(velocity.x - friction.x * deltaTime, 0); } else { velocity.x = Math.min(velocity.x + friction.x * deltaTime, 0); } } // Apply acceleration velocity.x += acceleration.x * deltaTime; // Make sure the object's velocity does not exceed the // positive or negative terminal velocity velocity.x = MathUtils.clamp(velocity.x, -terminalVelocity.x, terminalVelocity.x); } protected void updateMotionY(float deltaTime) { if (velocity.y != 0) { // Apply friction if (velocity.y > 0) { velocity.y = Math.max(velocity.y - friction.y * deltaTime, 0); } else { velocity.y = Math.min(velocity.y + friction.y * deltaTime, 0); } } // Apply acceleration velocity.y += acceleration.y * deltaTime; // Make sure the object's velocity does not exceed the // positive or negative terminal velocity velocity.y = MathUtils.clamp(velocity.y, -terminalVelocity.y, terminalVelocity.y); }
然后在update里调用:
updateMotionX(deltaTime); updateMotionY(deltaTime); // Move to new position position.x += velocity.x * deltaTime; position.y += velocity.y * deltaTime;
这样所有的子类都拥有了这些物理特性。
添加金币:
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 GoldCoin extends AbstractGameObject { private TextureRegion regGoldCoin; public boolean collected; public GoldCoin() { init(); } private void init() { dimension.set(0.5f, 0.5f); regGoldCoin = Assets.instance.goldCoin.goldCoin; // Set bounding box for collision detection bounds.set(0, 0, dimension.x, dimension.y); collected = false; } public void render(SpriteBatch batch) { if (collected) return; TextureRegion reg = null; reg = regGoldCoin; batch.draw(reg.getTexture(), position.x, position.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); } public int getScore() { return 100; } }
金币就一个图片,供玩家收集,收集一个就得100分,然后消失。
创建羽毛:
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 Feather extends AbstractGameObject { private TextureRegion regFeather; public boolean collected; public Feather() { init(); } private void init() { dimension.set(0.5f, 0.5f); regFeather = Assets.instance.feather.feather; // Set bounding box for collision detection bounds.set(0, 0, dimension.x, dimension.y); collected = false; } public void render(SpriteBatch batch) { if (collected) return; TextureRegion reg = null; reg = regFeather; batch.draw(reg.getTexture(), position.x, position.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); } public int getScore() { return 250; } }
它的代码几乎和金币一样,就是分数高点。
创建兔子头:
我们需要根据羽毛来激活是否兔子头应该powerUp,当powerUp的时候,我们把兔子头变成黄色。
package com.packtpub.libgdx.canyonbunny.game.objects; import com.badlogic.gdx.Gdx; 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.util.Constants; public class BunnyHead extends AbstractGameObject { public static final String TAG = BunnyHead.class.getName(); private final float JUMP_TIME_MAX = 0.3f; private final float JUMP_TIME_MIN = 0.1f; private final float JUMP_TIME_OFFSET_FLYING = JUMP_TIME_MAX - 0.018f; public enum VIEW_DIRECTION { LEFT, RIGHT } public enum JUMP_STATE { GROUNDED, FALLING, JUMP_RISING, JUMP_FALLING } private TextureRegion regHead; public VIEW_DIRECTION viewDirection; public float timeJumping; public JUMP_STATE jumpState; public boolean hasFeatherPowerup; public float timeLeftFeatherPowerup; public BunnyHead() { init(); } public void init() { dimension.set(1, 1); regHead = Assets.instance.bunny.head; // Center image on game object origin.set(dimension.x / 2, dimension.y / 2); // Bounding box for collision detection bounds.set(0, 0, dimension.x, dimension.y); // Set physics values terminalVelocity.set(3.0f, 4.0f); friction.set(12.0f, 0.0f); acceleration.set(0.0f, -25.0f); // View direction viewDirection = VIEW_DIRECTION.RIGHT; // Jump state jumpState = JUMP_STATE.FALLING; timeJumping = 0; // Power-ups hasFeatherPowerup = false; timeLeftFeatherPowerup = 0; } public void setJumping(boolean jumpKeyPressed) { switch (jumpState) { case GROUNDED: // Character is standing on a platform if (jumpKeyPressed) { // Start counting jump time from the beginning timeJumping = 0; jumpState = JUMP_STATE.JUMP_RISING; } break; case JUMP_RISING: // Rising in the air if (!jumpKeyPressed) jumpState = JUMP_STATE.JUMP_FALLING; break; case FALLING:// Falling down case JUMP_FALLING: // Falling down after jump if (jumpKeyPressed && hasFeatherPowerup) { timeJumping = JUMP_TIME_OFFSET_FLYING; jumpState = JUMP_STATE.JUMP_RISING; } break; } } public void setFeatherPowerup(boolean pickedUp) { hasFeatherPowerup = pickedUp; if (pickedUp) { timeLeftFeatherPowerup = Constants.ITEM_FEATHER_POWERUP_DURATION; } } public boolean hasFeatherPowerup() { return hasFeatherPowerup && timeLeftFeatherPowerup > 0; } @Override public void update(float deltaTime) { super.update(deltaTime); if (velocity.x != 0) { viewDirection = velocity.x < 0 ? VIEW_DIRECTION.LEFT : VIEW_DIRECTION.RIGHT; } if (timeLeftFeatherPowerup > 0) { timeLeftFeatherPowerup -= deltaTime; if (timeLeftFeatherPowerup < 0) { // disable power-up timeLeftFeatherPowerup = 0; setFeatherPowerup(false); } } } @Override protected void updateMotionY(float deltaTime) { switch (jumpState) { case GROUNDED: jumpState = JUMP_STATE.FALLING; break; case JUMP_RISING: // Keep track of jump time timeJumping += deltaTime; // Jump time left? if (timeJumping <= JUMP_TIME_MAX) { // Still jumping velocity.y = terminalVelocity.y; } break; case FALLING: break; case JUMP_FALLING: // Add delta times to track jump time timeJumping += deltaTime; // Jump to minimal height if jump key was pressed too short if (timeJumping > 0 && timeJumping <= JUMP_TIME_MIN) { // Still jumping velocity.y = terminalVelocity.y; } } if (jumpState != JUMP_STATE.GROUNDED) super.updateMotionY(deltaTime); } @Override public void render(SpriteBatch batch) { TextureRegion reg = null; // Set special color when game object has a feather power-up if (hasFeatherPowerup) batch.setColor(1.0f, 0.8f, 0.0f, 1.0f); // Draw image reg = regHead; batch.draw(reg.getTexture(), position.x, position.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), viewDirection == VIEW_DIRECTION.LEFT, false); // Reset color to white batch.setColor(1, 1, 1, 1); } }
不太明白的可以看流程图:
setJumping的流程:
更新岩石:
public void setLength(int length) { this.length = length; // Update bounding box for collision detection bounds.set(0, 0, dimension.x * length, dimension.y); }
修改Level的init()和render()
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.Clouds; import com.packtpub.libgdx.canyonbunny.game.objects.Feather; 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 enum BLOCK_TYPE { 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>(); // 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); } // 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); clouds.update(deltaTime); } public void render(SpriteBatch batch) { // Draw Mountains mountains.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 Player Character bunnyHead.render(batch); // Draw Water Overlay waterOverlay.render(batch); // Draw Clouds clouds.render(batch); } }
最后修改controller的update:level.update(deltaTime);
增加游戏逻辑:在相应的object加上碰撞方法和相应处理
也可以都交给controller,比如在WorldController中加入:
// Rectangles for collision detection private Rectangle r1 = new Rectangle(); private Rectangle r2 = new Rectangle(); private void onCollisionBunnyHeadWithRock(Rock rock) { }; private void onCollisionBunnyWithGoldCoin(GoldCoin goldcoin) { }; private void onCollisionBunnyWithFeather(Feather feather) { }; private void testCollisions() { r1.set(level.bunnyHead.position.x, level.bunnyHead.position.y, level.bunnyHead.bounds.width, level.bunnyHead.bounds.height); // Test collision: Bunny Head <-> Rocks for (Rock rock : level.rocks) { r2.set(rock.position.x, rock.position.y, rock.bounds.width, rock.bounds.height); if (!r1.overlaps(r2)) continue; onCollisionBunnyHeadWithRock(rock); // IMPORTANT: must do all collisions for valid // edge testing on rocks. } // Test collision: Bunny Head <-> Gold Coins for (GoldCoin goldcoin : level.goldcoins) { if (goldcoin.collected) continue; r2.set(goldcoin.position.x, goldcoin.position.y, goldcoin.bounds.width, goldcoin.bounds.height); if (!r1.overlaps(r2)) continue; onCollisionBunnyWithGoldCoin(goldcoin); break; } // Test collision: Bunny Head <-> Feathers for (Feather feather : level.feathers) { if (feather.collected) continue; r2.set(feather.position.x, feather.position.y, feather.bounds.width, feather.bounds.height); if (!r1.overlaps(r2)) continue; onCollisionBunnyWithFeather(feather); break; } }
现在3个碰撞事件都是空的,实现逻辑是这样的:
private void onCollisionBunnyHeadWithRock(Rock rock) { BunnyHead bunnyHead = level.bunnyHead; float heightDifference = Math.abs(bunnyHead.position.y - (rock.position.y + rock.bounds.height)); if (heightDifference > 0.25f) { boolean hitLeftEdge = bunnyHead.position.x > (rock.position.x + rock.bounds.width / 2.0f); if (hitLeftEdge) { bunnyHead.position.x = rock.position.x + rock.bounds.width; } else { bunnyHead.position.x = rock.position.x - bunnyHead.bounds.width; } return; } switch (bunnyHead.jumpState) { case GROUNDED: break; case FALLING: case JUMP_FALLING: bunnyHead.position.y = rock.position.y + bunnyHead.bounds.height + bunnyHead.origin.y; bunnyHead.jumpState = JUMP_STATE.GROUNDED; break; case JUMP_RISING: bunnyHead.position.y = rock.position.y + bunnyHead.bounds.height + bunnyHead.origin.y; break; } } private void onCollisionBunnyWithGoldCoin(GoldCoin goldcoin) { goldcoin.collected = true; score += goldcoin.getScore(); Gdx.app.log(TAG, "Gold coin collected"); } private void onCollisionBunnyWithFeather(Feather feather) { feather.collected = true; score += feather.getScore(); level.bunnyHead.setFeatherPowerup(true); Gdx.app.log(TAG, "Feather collected"); }
试试效果,在WorldController的update加入:testCollisions();
跑起来,看看:
操作键:上,下,左,右,逗号,点等键。
最后,我们跟前面一样,让摄像机跟随兔子头:在Controller的initLevel中添加cameraHelper.setTarget(level.bunnyHead);
修改handleDebugInput()和keyUp():
@Override public boolean keyUp(int keycode) { if (keycode == Keys.R) { init(); Gdx.app.debug(TAG, "Game World Resetted!"); }// Toggle camera follow else if (keycode == Keys.ENTER) { cameraHelper.setTarget(cameraHelper.hasTarget() ? null : level.bunnyHead); Gdx.app.debug(TAG, "Camera follow enabled: " + cameraHelper.hasTarget()); } return false; } private void handleInputGame(float deltaTime) { if (cameraHelper.hasTarget(level.bunnyHead)) { // Player Movement if (Gdx.input.isKeyPressed(Keys.LEFT)) { level.bunnyHead.velocity.x = -level.bunnyHead.terminalVelocity.x; } else if (Gdx.input.isKeyPressed(Keys.RIGHT)) { level.bunnyHead.velocity.x = level.bunnyHead.terminalVelocity.x; } else { // Execute auto-forward movement on non-desktop platform if (Gdx.app.getType() != ApplicationType.Desktop) { level.bunnyHead.velocity.x = level.bunnyHead.terminalVelocity.x; } } // Bunny Jump if (Gdx.input.isTouched() || Gdx.input.isKeyPressed(Keys.SPACE)) level.bunnyHead.setJumping(true); else level.bunnyHead.setJumping(false); } } private void handleDebugInput(float deltaTime) { if (Gdx.app.getType() != ApplicationType.Desktop) return; if (!cameraHelper.hasTarget(level.bunnyHead)) { // Camera Controls (move) float camMoveSpeed = 5 * deltaTime; float camMoveSpeedAccelerationFactor = 5; if (Gdx.input.isKeyPressed(Keys.SHIFT_LEFT)) camMoveSpeed *= camMoveSpeedAccelerationFactor; if (Gdx.input.isKeyPressed(Keys.LEFT)) moveCamera(-camMoveSpeed, 0); if (Gdx.input.isKeyPressed(Keys.RIGHT)) moveCamera(camMoveSpeed, 0); if (Gdx.input.isKeyPressed(Keys.UP)) moveCamera(0, camMoveSpeed); if (Gdx.input.isKeyPressed(Keys.DOWN)) moveCamera(0, -camMoveSpeed); if (Gdx.input.isKeyPressed(Keys.BACKSPACE)) cameraHelper.setPosition(0, 0); } // Camera Controls (zoom) float camZoomSpeed = 1 * deltaTime; float camZoomSpeedAccelerationFactor = 5; if (Gdx.input.isKeyPressed(Keys.SHIFT_LEFT)) camZoomSpeed *= camZoomSpeedAccelerationFactor; if (Gdx.input.isKeyPressed(Keys.COMMA)) cameraHelper.addZoom(camZoomSpeed); if (Gdx.input.isKeyPressed(Keys.PERIOD)) cameraHelper.addZoom(-camZoomSpeed); if (Gdx.input.isKeyPressed(Keys.SLASH)) cameraHelper.setZoom(1); }
在update里加上handleInputGame(deltaTime);
大家都知道有更加成熟的Box2D可以仿真物理世界。但是我们的兔子游戏并不是完全精确的仿真物理,甚至还有比较超物理的动作,如果用Box2D来模拟不真实的物理效果是很麻烦和多余的。
掉命,游戏结束以及其他:
我们通常都会在游戏结束之后经过3秒的过渡再跳转画面。
首先定义常量在Constants:// Delay after game over
public static final float TIME_DELAY_GAME_OVER = 3;
把下面的代码加到WorldController:判断玩家掉水里了没
private float timeLeftGameOverDelay; public boolean isGameOver() { return lives < 0; } public boolean isPlayerInWater() { return level.bunnyHead.position.y < -5; }
修改init()和update():死掉一条命,右上角的小兔头就变透明
private void init() { Gdx.input.setInputProcessor(this); cameraHelper = new CameraHelper(); lives = Constants.LIVES_START; timeLeftGameOverDelay = 0; initLevel(); } public void update(float deltaTime) { handleDebugInput(deltaTime); if (isGameOver()) { timeLeftGameOverDelay -= deltaTime; if (timeLeftGameOverDelay < 0) init(); } else { handleInputGame(deltaTime); } level.update(deltaTime); testCollisions(); cameraHelper.update(deltaTime); if (!isGameOver() && isPlayerInWater()) { lives--; if (isGameOver()) timeLeftGameOverDelay = Constants.TIME_DELAY_GAME_OVER; else initLevel(); } }
看看效果怎么样。
为了防止摄像机向下移动的太快,修改CameraHelpe的update()
public void update (float deltaTime) { if (!hasTarget()) return; position.x = target.position.x + target.origin.x; position.y = target.position.y + target.origin.y; // Prevent camera from moving down too far position.y = Math.max(-1f, position.y); }
在WorldRenderer中加上game over的信息:
private void renderGuiGameOverMessage(SpriteBatch batch) { float x = cameraGUI.viewportWidth / 2; float y = cameraGUI.viewportHeight / 2; if (worldController.isGameOver()) { BitmapFont fontGameOver = Assets.instance.fonts.defaultBig; fontGameOver.setColor(1, 0.75f, 0.25f, 1); fontGameOver.drawMultiLine(batch, "GAME OVER", x, y, 0, BitmapFont.HAlignment.CENTER); fontGameOver.setColor(1, 1, 1, 1); } }
加上兔子捡到羽毛的剩余时间:
private void renderGuiFeatherPowerup(SpriteBatch batch) { float x = -15; float y = 30; float timeLeftFeatherPowerup = worldController.level.bunnyHead.timeLeftFeatherPowerup; if (timeLeftFeatherPowerup > 0) { // Start icon fade in/out if the left power-up time // is less than 4 seconds. The fade interval is set // to 5 changes per second. if (timeLeftFeatherPowerup < 4) { if (((int) (timeLeftFeatherPowerup * 5) % 2) != 0) { batch.setColor(1, 1, 1, 0.5f); } } batch.draw(Assets.instance.feather.feather, x, y, 50, 50, 100, 100, 0.35f, -0.35f, 0); batch.setColor(1, 1, 1, 1); Assets.instance.fonts.defaultSmall.draw(batch, "" + (int) timeLeftFeatherPowerup, x + 60, y + 57); } }
然后把新加的这两个方法加入到renderGUI();
private void renderGui (SpriteBatch batch) { batch.setProjectionMatrix(cameraGUI.combined); batch.begin(); // draw collected gold coins icon + text // (anchored to top left edge) renderGuiScore(batch); // draw collected feather icon (anchored to top left edge) renderGuiFeatherPowerup(batch); // draw extra lives icon + text (anchored to top right edge) renderGuiExtraLive(batch); // draw FPS text (anchored to bottom right edge) renderGuiFpsCounter(batch); // draw game over text renderGuiGameOverMessage(batch); batch.end(); }
Nice!到这里,游戏的主体基本完成了。犒赏一下自己,嗯,实在太棒了!多玩一玩自己的杰作,等休息好了我们再继续。
在下一章,我们将给游戏加上进入游戏之前的菜单界面,让游戏完整。