• [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。

    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);

    游戏的基本功能到此完成。

    当然,基本功能的完成一般就意味着游戏才完成了一半,更多工作需要继续...

    素材下载:http://files.cnblogs.com/mignet/images.zip

  • 相关阅读:
    【2020-11-01】从身边人开始输出自己的价值
    【一句日历】2020年11月
    【2020-10-31】继续解锁自己内心的矛盾
    【2020-10-29】静下心来,书中自有黄金
    【2020-10-28】平凡人终归还是要回归到小日子上
    【2020-10-27】抗衡自己的摇摆幅度
    【2020-10-26】市场驱动学习和进步
    【2020-10-25】窜着野炊的心干着农民的活
    暑假集训2016day3T1 欧拉回路(UOJ #117欧拉回路)(史上最全的欧拉回路纯无向图/有向图解析)
    leetcode1282
  • 原文地址:https://www.cnblogs.com/mignet/p/libgdx_game_development_07.html
Copyright © 2020-2023  润新知