管理多个屏幕

我们的菜单屏有2个按钮,一个play一个option。option里就是一些开关的设置,比如音乐音效等。这些设置将会保存到Preferences中。

多屏幕切换是游戏的基本机制,Libgdx提供了一个叫Game的类已经具有了这样的功能。

为了适应多屏幕的功能,我们的类图需要做一些修改:

[libgdx游戏开发教程]使用Libgdx进行游戏开发(7)-屏幕布局的最佳实践

改动在: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的样子,准备创建了。

 [libgdx游戏开发教程]使用Libgdx进行游戏开发(7)-屏幕布局的最佳实践

接下来就是这个富有特色的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文件存储在一个单独的文件中.
详细描述

相关文章: