[libgdx遊戲開發教程]使用Libgdx進行遊戲開發(7)-Screen2D屏幕布局的最佳實踐
管理多個螢幕
我們的選單屏有2個按鈕,一個play一個option。option裡就是一些開關的設定,比如音樂音效等。這些設定將會儲存到Preferences中。
多螢幕切換是遊戲的基本機制,Libgdx提供了一個叫Game的類已經具有了這樣的功能。
為了適應多螢幕的功能,我們的類圖需要做一些修改:
改動在:CanyonBunnyMain不再實現ApplicationListener介面,而是繼承自Game類。這個類提供了setScreen()方法來進行切換。
我們定義抽象的AbstractGameScreen來統一共同的行為。同時,它實現了Libgdx的Screen介面(show,hide)。
GameScreen將取代CanyonBunnyMain的位置。
開始編寫類AbstractGameScreen:
package com.packtpub.libgdx.canyonbunny.screens; import com.badlogic.gdx.Game; import com.badlogic.gdx.Screen; import com.badlogic.gdx.assets.AssetManager; import com.packtpub.libgdx.canyonbunny.game.Assets; public abstract class AbstractGameScreen implements Screen { protected Game game; public AbstractGameScreen(Game game) { this.game = game; } public abstract void render(float deltaTime); public abstract void resize(int width, int height); public abstract void show(); public abstract void hide(); public abstract void pause(); public void resume() { Assets.instance.init(new AssetManager()); } public void dispose() { Assets.instance.dispose(); } }
GameScreen把職責拿過來:
package com.packtpub.libgdx.canyonbunny.screens; import com.badlogic.gdx.Game; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL10; import com.packtpub.libgdx.canyonbunny.game.WorldController; import com.packtpub.libgdx.canyonbunny.game.WorldRenderer; public class GameScreen extends AbstractGameScreen { private static final String TAG = GameScreen.class.getName(); private WorldController worldController; private WorldRenderer worldRenderer; private boolean paused; public GameScreen(Game game) { super(game); } @Override public void render(float deltaTime) { // Do not update game world when paused. if (!paused) { // Update game world by the time that has passed // since last rendered frame. worldController.update(deltaTime); } // Sets the clear screen color to: Cornflower Blue Gdx.gl.glClearColor(0x64 / 255.0f, 0x95 / 255.0f, 0xed / 255.0f, 0xff / 255.0f); // Clears the screen Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // Render game world to screen worldRenderer.render(); } @Override public void resize(int width, int height) { worldRenderer.resize(width, height); } @Override public void show() { worldController = new WorldController(game); worldRenderer = new WorldRenderer(worldController); Gdx.input.setCatchBackKey(true); } @Override public void hide() { worldRenderer.dispose(); Gdx.input.setCatchBackKey(false); } @Override public void pause() { paused = true; } @Override public void resume() { super.resume(); // Only called on Android! paused = false; } }
那麼CanyonBunnyMain就瘦身了:
package com.packtpub.libgdx.canyonbunny;
import com.badlogic.gdx.Application;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.AssetManager;
import com.packtpub.libgdx.canyonbunny.game.Assets;
import com.packtpub.libgdx.canyonbunny.screens.MenuScreen;
public class CanyonBunnyMain extends Game {
@Override
public void create() {
// Set Libgdx log level
Gdx.app.setLogLevel(Application.LOG_DEBUG);
// Load assets
Assets.instance.init(new AssetManager());
// Start game at menu screen
setScreen(new MenuScreen(this));
}
}
WorldController開始持有game的引用,以便於跳轉;
package com.packtpub.libgdx.canyonbunny.game;
import com.badlogic.gdx.Application.ApplicationType;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.math.Rectangle;
import com.packtpub.libgdx.canyonbunny.game.objects.BunnyHead;
import com.packtpub.libgdx.canyonbunny.game.objects.BunnyHead.JUMP_STATE;
import com.packtpub.libgdx.canyonbunny.game.objects.Feather;
import com.packtpub.libgdx.canyonbunny.game.objects.GoldCoin;
import com.packtpub.libgdx.canyonbunny.game.objects.Rock;
import com.packtpub.libgdx.canyonbunny.screens.MenuScreen;
import com.packtpub.libgdx.canyonbunny.util.CameraHelper;
import com.packtpub.libgdx.canyonbunny.util.Constants;
public class WorldController extends InputAdapter {
private static final String TAG = WorldController.class.getName();
public CameraHelper cameraHelper;
public Level level;
public int lives;
public int score;
private float timeLeftGameOverDelay;
private Game game;
private void backToMenu() {
// switch to menu screen
game.setScreen(new MenuScreen(game));
}
public boolean isGameOver() {
return lives < 0;
}
public boolean isPlayerInWater() {
return level.bunnyHead.position.y < -5;
}
private void initLevel() {
score = 0;
level = new Level(Constants.LEVEL_01);
cameraHelper.setTarget(level.bunnyHead);
}
// Rectangles for collision detection
private Rectangle r1 = new Rectangle();
private Rectangle r2 = new Rectangle();
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");
}
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;
}
}
public WorldController(Game game) {
this.game = game;
Gdx.input.setInputProcessor(this);
init();
}
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);
}
private void moveCamera(float x, float y) {
x += cameraHelper.getPosition().x;
y += cameraHelper.getPosition().y;
cameraHelper.setPosition(x, y);
}
@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());
}
// Back to Menu
else if (keycode == Keys.ESCAPE || keycode == Keys.BACK) {
backToMenu();
}
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 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)
backToMenu();
} else {
handleInputGame(deltaTime);
}
level.update(deltaTime);
testCollisions();
cameraHelper.update(deltaTime);
if (!isGameOver() && isPlayerInWater()) {
lives--;
if (isGameOver())
timeLeftGameOverDelay = Constants.TIME_DELAY_GAME_OVER;
else
initLevel();
}
}
private Pixmap createProceduralPixmap(int width, int height) {
Pixmap pixmap = new Pixmap(width, height, Format.RGBA8888);
// Fill square with red color at 50% opacity
pixmap.setColor(1, 0, 0, 0.5f);
pixmap.fill();
// Draw a yellow-colored X shape on square
pixmap.setColor(1, 1, 0, 1);
pixmap.drawLine(0, 0, width, height);
pixmap.drawLine(width, 0, 0, height);
// Draw a cyan-colored border around square
pixmap.setColor(0, 1, 1, 1);
pixmap.drawRectangle(0, 0, width, height);
return pixmap;
}
}
現在,構思下開始選單的樣子,準備建立了。
接下來就是這個富有特色的MenuScreen的建立了。首先要準備圖片和載入,和前文一樣打包。然後使用一個JSON檔案來定義Menu的面板。
比如我們起名叫:canyonbunnyui.json
{ com.badlogic.gdx.scenes.scene2d.ui.Button$ButtonStyle: { play: { down: play-dn, up: play-up }, options: { down: options-dn, up: options-up } }, com.badlogic.gdx.scenes.scene2d.ui.Image: { background: { drawable: background }, logo: { drawable: logo }, info: { drawable: info }, coins: { drawable: coins }, bunny: { drawable: bunny }, }, }
增加常量到Constants:
public static final String TEXTURE_ATLAS_UI = "images/canyonbunny-ui.pack";
public static final String TEXTURE_ATLAS_LIBGDX_UI = "images/uiskin.atlas";
// Location of description file for skins
public static final String SKIN_LIBGDX_UI = "images/uiskin.json";
public static final String SKIN_CANYONBUNNY_UI = "images/canyonbunny-ui.json";
Libgdx構建Scene2D (UI),使用的特性就是TableLayout和skins。
Libgdx附帶了一個很牛叉的工具組來讓開發者很容易建立場景. 場景的層次組織結構很像硬碟上資料夾檔案的結構.在Libgdx裡,這些物件被稱為演員Actor.
演員可以相互巢狀來組成演員組. 演員組是一個非常有用的特性, 因為任何對父Actor的改動,都會應用到他的子Actor. 此外, 每個演員都有自己的座標系, 這就使得定義演員組裡的成員的相對偏移量變得很容易(無論是位置,旋轉角度還是縮放).
Scene2D支援已經旋轉或者縮放的Actor的碰撞檢測. Libgdx靈活的事件系統允許按需處理和分發輸入事件以便父Actor可以在輸入事件到達子Actor之前攔截它. 最後, 內建的action系統可以很容易用來操縱actors,
也可以通過執行動作序列來完成複雜的效果,平移, 或者是兩者組合. 所有這些描述的功能都封裝在Stage類, 它包含層次結構和分發使用者的事件. 在任何時候,Actor都能夠加入它或者從它移除.
Stage類和Actor類都包含act()方法,這個方法得到一個時間作為引數然後執行基於時間的動作。呼叫Stage的act()將會引起整個場景的act()呼叫。
Stage和Actor的act()方法其實基本上和我們所知道的update()方法一樣,只是用了一個不同的名字. 更多關於Scene2D, 參考官方文件https://code.google.com/p/libgdx/wiki/scene2d/.
到目前為止, 在我們的遊戲中我們沒有使用任何的Scene2D的這些特性, 雖然我們都已經用Scene2D的物件實現了遊戲的場景。記住,使用場景有一定的開銷. Libgdx試圖全力保持開銷在最低的程度,比如: 如果物件不需要旋轉和縮放就跳過複雜的轉換矩陣的計算. 所以, 這取決於你的需求.
我們要建立的選單很複雜,我們直接用libgdx已經支援的 Scene2D UI來做. 如果有特殊需要,我們還可以繼承這些UI,實現它們的介面,以增強它們的功能.
在Libgdx中, 這些UI元素都叫做元件widgets.
下面是所有在當前Scene2D UI有效的widget簡表:
Button, CheckBox, Dialog, Image, ImageButton, Label, List, ScrollPane,SelectBox, Slider, SplitPane, Stack, Window, TextButton, TextField,Touchpad 和 Tree.
Scene2D UI 也支援簡單的建立新的自定義的widgets種類.
我們將只涉及我們的選單中將要用到的一些widget.
完整描述每一個widget的列表,請參考官方文件https://code.google.com/p/libgdx/wiki/scene2dui/.
除了Scene2D UI, Libgdx還集成了一個單獨的專案--TableLayout.
TableLayout使用Tables很容易建立和維護動態的(或者叫與解析度無關的)佈局,也提供了很直觀的API. Table提供了訪問TableLayout的功能, 同時Table也實現了作為widget的功能, 因此Table可以完全無縫整合到Scene2D的UI中.
強烈推薦去看官方文件https://code.google.com/p/table-layout/.
Scene2D UI另一個重要的特徵就是支援面板skins.
面板是資源的集合,包括樣式和UI元件. 資源可以是texture regions(紋理區域), fonts(字型)和 colors(顏色). 通常來講, 面板使用的紋理區域,來自一個紋理集. 每個部件的樣式定義使用JSON檔案儲存在一個單獨的檔案中.
我們現在來實際的實現Menu屏,首先來看一下層級關係:
場景圖從一個空的Stage開始. 然後,第一個新增到stage的子actor是一個Stack. Stack允許你新增可以相互覆蓋的actor. 我們將利用這一特性建立多個層. 每一層都使用一個Table作為父actor.
使用堆疊起來的table可以使我們能夠很容易和很邏輯性的佈局actor.
我們一步步來,先實現這個多層堆疊起來的結構(MenuScreen):
private Stage stage;
private Skin skinCanyonBunny;
// menu
private Image imgBackground;
private Image imgLogo;
private Image imgInfo;
private Image imgCoins;
private Image imgBunny;
private Button btnMenuPlay;
private Button btnMenuOptions;
// options
private Window winOptions;
private TextButton btnWinOptSave;
private TextButton btnWinOptCancel;
private CheckBox chkSound;
private Slider sldSound;
private CheckBox chkMusic;
private Slider sldMusic;
private SelectBox selCharSkin;
private Image imgCharSkin;
private CheckBox chkShowFpsCounter;
// debug
private final float DEBUG_REBUILD_INTERVAL = 5.0f;
private boolean debugEnabled = false;
private float debugRebuildStage;
private void rebuildStage() {
skinCanyonBunny = new Skin(
Gdx.files.internal(Constants.SKIN_CANYONBUNNY_UI),
new TextureAtlas(Constants.TEXTURE_ATLAS_UI));
// build all layers
Table layerBackground = buildBackgroundLayer();
Table layerObjects = buildObjectsLayer();
Table layerLogos = buildLogosLayer();
Table layerControls = buildControlsLayer();
Table layerOptionsWindow = buildOptionsWindowLayer();
// assemble stage for menu screen
stage.clear();
Stack stack = new Stack();
stage.addActor(stack);
stack.setSize(Constants.VIEWPORT_GUI_WIDTH,
Constants.VIEWPORT_GUI_HEIGHT);
stack.add(layerBackground);
stack.add(layerObjects);
stack.add(layerLogos);
stack.add(layerControls);
stage.addActor(layerOptionsWindow);
}
private Table buildBackgroundLayer() {
Table layer = new Table();
return layer;
}
private Table buildObjectsLayer() {
Table layer = new Table();
return layer;
}
private Table buildLogosLayer() {
Table layer = new Table();
return layer;
}
private Table buildControlsLayer() {
Table layer = new Table();
return layer;
}
private Table buildOptionsWindowLayer() {
Table layer = new Table();
return layer;
}
那麼,核心的問題是,怎麼讓這一套理論來實現的東東能夠適應各種螢幕size呢?修改下面程式碼
@Override
public void resize(int width, int height) {
stage.setViewport(Constants.VIEWPORT_GUI_WIDTH,
Constants.VIEWPORT_GUI_HEIGHT, false);
}
@Override
public void hide() {
stage.dispose();
skinCanyonBunny.dispose();
}
@Override
public void show() {
stage = new Stage();
Gdx.input.setInputProcessor(stage);
rebuildStage();
}
給menu加上debug的程式碼:
@Override
public void render(float deltaTime) {
Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
if (debugEnabled) {
debugRebuildStage -= deltaTime;
if (debugRebuildStage <= 0) {
debugRebuildStage = DEBUG_REBUILD_INTERVAL;
rebuildStage();
}
}
stage.act(deltaTime);
stage.draw();
//Table.drawDebug(stage);新版中是stage.setDebug
}
不要小看這裡的debug程式碼,在開啟debug的情況下它會在你設定的間隔時間就rebuild我們的stage,也就是說你可以在執行的時候做更新。(JVM的程式碼熱交換特性)
比如你正在調整某個menu的位置,直接改配置檔案,不用重啟就可以看效果,這將節省大量的時間。
接下來,一一實現每一層具體的功能。
首先是背景層,加上背景圖片:
private Table buildBackgroundLayer() {
Table layer = new Table();
// + Background
imgBackground = new Image(skinCanyonBunny, "background");
layer.add(imgBackground);
return layer;
}
然後是Object層:
private Table buildObjectsLayer() {
Table layer = new Table();
// + Coins
imgCoins = new Image(skinCanyonBunny, "coins");
layer.addActor(imgCoins);
imgCoins.setPosition(135, 80);
// + Bunny
imgBunny = new Image(skinCanyonBunny, "bunny");
layer.addActor(imgBunny);
imgBunny.setPosition(355, 40);
return layer;
}
接著是logo層:
private Table buildLogosLayer() {
Table layer = new Table();
layer.left().top();
// + Game Logo
imgLogo = new Image(skinCanyonBunny, "logo");
layer.add(imgLogo);
layer.row().expandY();
// + Info Logos
imgInfo = new Image(skinCanyonBunny, "info");
layer.add(imgInfo).bottom();
if (debugEnabled)
layer.debug();
return layer;
}
接著是控制層:按鈕或者選單層
private Table buildControlsLayer() {
Table layer = new Table();
layer.right().bottom();
// + Play Button
btnMenuPlay = new Button(skinCanyonBunny, "play");
layer.add(btnMenuPlay);
btnMenuPlay.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
onPlayClicked();
}
});
layer.row();
// + Options Button
btnMenuOptions = new Button(skinCanyonBunny, "options");
layer.add(btnMenuOptions);
btnMenuOptions.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
onOptionsClicked();
}
});
if (debugEnabled)
layer.debug();
return layer;
}
private void onPlayClicked() {
game.setScreen(new GameScreen(game));
}
private void onOptionsClicked() {
}
新增選項層:
這個option使用的素材是Libgdx預設的素材:
• uiskin.png
• uiskin.atlas
• uiskin.json
• default.fnt
為了儲存玩家選擇的結果,我們新建一個GamePreferences的類來儲存使用者資料:
package com.packtpub.libgdx.canyonbunny.util;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Preferences;
import com.badlogic.gdx.math.MathUtils;
public class GamePreferences {
public static final String TAG = GamePreferences.class.getName();
public static final GamePreferences instance = new GamePreferences();
public boolean sound;
public boolean music;
public float volSound;
public float volMusic;
public int charSkin;
public boolean showFpsCounter;
private Preferences prefs;
// singleton: prevent instantiation from other classes
private GamePreferences() {
prefs = Gdx.app.getPreferences(Constants.PREFERENCES);
}
public void load() {
sound = prefs.getBoolean("sound", true);
music = prefs.getBoolean("music", true);
volSound = MathUtils
.clamp(prefs.getFloat("volSound", 0.5f), 0.0f, 1.0f);
volMusic = MathUtils
.clamp(prefs.getFloat("volMusic", 0.5f), 0.0f, 1.0f);
charSkin = MathUtils.clamp(prefs.getInteger("charSkin", 0), 0, 2);
showFpsCounter = prefs.getBoolean("showFpsCounter", false);
}
public void save() {
prefs.putBoolean("sound", sound);
prefs.putBoolean("music", music);
prefs.putFloat("volSound", volSound);
prefs.putFloat("volMusic", volMusic);
prefs.putInteger("charSkin", charSkin);
prefs.putBoolean("showFpsCounter", showFpsCounter);
prefs.flush();
}
}
很眼熟吧,不錯,跟cocos裡的userdata一樣,都是用xml檔案在儲存。
建立一個可選擇項的面板類CharacterSkin,讓兔子頭可以換膚:
package com.packtpub.libgdx.canyonbunny.util;
import com.badlogic.gdx.graphics.Color;
public enum CharacterSkin {
WHITE("White", 1.0f, 1.0f, 1.0f), GRAY("Gray", 0.7f, 0.7f, 0.7f), BROWN(
"Brown", 0.7f, 0.5f, 0.3f);
private String name;
private Color color = new Color();
private CharacterSkin(String name, float r, float g, float b) {
this.name = name;
color.set(r, g, b, 1.0f);
}
@Override
public String toString() {
return name;
}
public Color getColor() {
return color;
}
}
給menu屏加上option層的程式碼:
private Skin skinLibgdx;
private void loadSettings() {
GamePreferences prefs = GamePreferences.instance;
prefs.load();
chkSound.setChecked(prefs.sound);
sldSound.setValue(prefs.volSound);
chkMusic.setChecked(prefs.music);
sldMusic.setValue(prefs.volMusic);
selCharSkin.setSelection(prefs.charSkin);
onCharSkinSelected(prefs.charSkin);
chkShowFpsCounter.setChecked(prefs.showFpsCounter);
}
private void saveSettings() {
GamePreferences prefs = GamePreferences.instance;
prefs.sound = chkSound.isChecked();
prefs.volSound = sldSound.getValue();
prefs.music = chkMusic.isChecked();
prefs.volMusic = sldMusic.getValue();
prefs.charSkin = selCharSkin.getSelectionIndex();
prefs.showFpsCounter = chkShowFpsCounter.isChecked();
prefs.save();
}
private void onCharSkinSelected(int index) {
CharacterSkin skin = CharacterSkin.values()[index];
imgCharSkin.setColor(skin.getColor());
}
private void onSaveClicked() {
saveSettings();
onCancelClicked();
}
private void onCancelClicked() {
btnMenuPlay.setVisible(true);
btnMenuOptions.setVisible(true);
winOptions.setVisible(false);
}
在rebuildStage中加上:
skinLibgdx = new Skin(
Gdx.files.internal(Constants.SKIN_LIBGDX_UI),
new TextureAtlas(Constants.TEXTURE_ATLAS_LIBGDX_UI));
hide中加上:
skinLibgdx.dispose();
最後,來完成buildOptionWindowLayer():
private Table buildOptionsWindowLayer() {
winOptions = new Window("Options", skinLibgdx);
// + Audio Settings: Sound/Music CheckBox and Volume Slider
winOptions.add(buildOptWinAudioSettings()).row();
// + Character Skin: Selection Box (White, Gray, Brown)
winOptions.add(buildOptWinSkinSelection()).row();
// + Debug: Show FPS Counter
winOptions.add(buildOptWinDebug()).row();
// + Separator and Buttons (Save, Cancel)
winOptions.add(buildOptWinButtons()).pad(10, 0, 10, 0);
// Make options window slightly transparent
winOptions.setColor(1, 1, 1, 0.8f);
// Hide options window by default
winOptions.setVisible(false);
if (debugEnabled)
winOptions.debug();
// Let TableLayout recalculate widget sizes and positions
winOptions.pack();
// Move options window to bottom right corner
winOptions.setPosition(
Constants.VIEWPORT_GUI_WIDTH - winOptions.getWidth() - 50, 50);
return winOptions;
}
private Table buildOptWinAudioSettings() {
Table tbl = new Table();
// + Title: "Audio"
tbl.pad(10, 10, 0, 10);
tbl.add(new Label("Audio", skinLibgdx, "default-font", Color.ORANGE))
.colspan(3);
tbl.row();
tbl.columnDefaults(0).padRight(10);
tbl.columnDefaults(1).padRight(10);
// + Checkbox, "Sound" label, sound volume slider
chkSound = new CheckBox("", skinLibgdx);
tbl.add(chkSound);
tbl.add(new Label("Sound", skinLibgdx));
sldSound = new Slider(0.0f, 1.0f, 0.1f, false, skinLibgdx);
tbl.add(sldSound);
tbl.row();
// + Checkbox, "Music" label, music volume slider
chkMusic = new CheckBox("", skinLibgdx);
tbl.add(chkMusic);
tbl.add(new Label("Music", skinLibgdx));
sldMusic = new Slider(0.0f, 1.0f, 0.1f, false, skinLibgdx);
tbl.add(sldMusic);
tbl.row();
return tbl;
}
private Table buildOptWinSkinSelection() {
Table tbl = new Table();
// + Title: "Character Skin"
tbl.pad(10, 10, 0, 10);
tbl.add(new Label("Character Skin", skinLibgdx, "default-font",
Color.ORANGE)).colspan(2);
tbl.row();
// + Drop down box filled with skin items
selCharSkin = new SelectBox(CharacterSkin.values(), skinLibgdx);
selCharSkin.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
onCharSkinSelected(((SelectBox) actor).getSelectionIndex());
}
});
tbl.add(selCharSkin).width(120).padRight(20);
// + Skin preview image
imgCharSkin = new Image(Assets.instance.bunny.head);
tbl.add(imgCharSkin).width(50).height(50);
return tbl;
}
private Table buildOptWinDebug() {
Table tbl = new Table();
// + Title: "Debug"
tbl.pad(10, 10, 0, 10);
tbl.add(new Label("Debug", skinLibgdx, "default-font", Color.RED))
.colspan(3);
tbl.row();
tbl.columnDefaults(0).padRight(10);
tbl.columnDefaults(1).padRight(10);
// + Checkbox, "Show FPS Counter" label
chkShowFpsCounter = new CheckBox("", skinLibgdx);
tbl.add(new Label("Show FPS Counter", skinLibgdx));
tbl.add(chkShowFpsCounter);
tbl.row();
return tbl;
}
private Table buildOptWinButtons() {
Table tbl = new Table();
// + Separator
Label lbl = null;
lbl = new Label("", skinLibgdx);
lbl.setColor(0.75f, 0.75f, 0.75f, 1);
lbl.setStyle(new LabelStyle(lbl.getStyle()));
lbl.getStyle().background = skinLibgdx.newDrawable("white");
tbl.add(lbl).colspan(2).height(1).width(220).pad(0, 0, 0, 1);
tbl.row();
lbl = new Label("", skinLibgdx);
lbl.setColor(0.5f, 0.5f, 0.5f, 1);
lbl.setStyle(new LabelStyle(lbl.getStyle()));
lbl.getStyle().background = skinLibgdx.newDrawable("white");
tbl.add(lbl).colspan(2).height(1).width(220).pad(0, 1, 5, 0);
tbl.row();
// + Save Button with event handler
btnWinOptSave = new TextButton("Save", skinLibgdx);
tbl.add(btnWinOptSave).padRight(30);
btnWinOptSave.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
onSaveClicked();
}
});
// + Cancel Button with event handler
btnWinOptCancel = new TextButton("Cancel", skinLibgdx);
tbl.add(btnWinOptCancel);
btnWinOptCancel.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
onCancelClicked();
}
});
return tbl;
}
補上onOptionClicked:
private void onOptionsClicked() {
loadSettings();
btnMenuPlay.setVisible(false);
btnMenuOptions.setVisible(false);
winOptions.setVisible(true);
}
要使用這些使用者設定,需要在show裡新增:GamePreferences.instance.load();
在兔子頭的類的render中新增:
// Apply Skin Color
batch.setColor(
CharacterSkin.values()[GamePreferences.instance.charSkin]
.getColor());
然後在worldrender裡的renderGui加上控制fps的設定:
if (GamePreferences.instance.showFpsCounter)
renderGuiFpsCounter(batch);
遊戲的基本功能到此完成。
在下一章我們將涉及更多的東西以便讓遊戲更有趣
PS:歡迎各路遊戲愛好者入群426950359