[libgdx游戏开发教程]使用Libgdx进行游戏开发(7)-屏幕布局的最佳实践
管理多个屏幕
我们的菜单屏有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; } }
现在,构思下menu screen的样子,准备创建了。
接下来就是这个富有特色的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。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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); }
不要小看这里的debug代码,在开启debug的情况下它会在你设定的间隔时间就rebuild我们的stage,也就是说你可以在运行的时候[desktop]做更新。(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);
游戏的基本功能到此完成。
当然,基本功能的完成一般就意味着游戏才完成了一半,更多工作需要继续...