[libgdx游戏开发教程]使用Libgdx进行游戏开发(9)-场景过渡

本章主要讲解场景过渡效果的使用。这里将用到Render to Texture(RTT)技术。

Libgdx提供了一个类,实现了各种常见的插值算法,不仅适合过渡效果,也适合任意特定行为。

在本游戏里面,我们将实现3种转场效果:fade, slide和slice.

和前面提到的多场景管理一样,我们也需要这样的结构来统一管理转场特效:

首先创建接口ScreenTransition:

package com.packtpub.libgdx.canyonbunny.screens.transitions;

import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

public interface ScreenTransition {
    public float getDuration();

    public void render(SpriteBatch batch, Texture currScreen,
            Texture nextScreen, float alpha);
}

OpenGL有个叫做Framebuffer Objects(FBO)的特性,它允许在内存中把离屏画面渲染到纹理中。

要使用这一特性,实例化Libgdx的Framebuffer类即可,一般的用法是:

// ...
Framebuffer fbo;
fbo = new Framebuffer(Format.RGB888, width, height, false);
fbo.begin(); // set render target to FBO's texture buffer
Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // solid black
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // clear FBO batch.
draw(someTextureRegion, 0, 0); // draw (to FBO)
fbo.end(); // revert render target back to normal
// retrieve result
Texture fboTexture = fbo.getColorBufferTexture();
// ...

其实这个特性是OpenGL ES 2.0模式下的,我们假设现在的硬件都支持了。哈哈。

为了让我们的Project支持OpenGL ES 2.0,需要做以下改动:

main方法:cfg.useGL20 = true;

把Android工程里的AndroidManifest.xml增加一行:

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

还有MainActivity里修改cfg.useGL20 = true;

HTML5项目就不用修改了,因为GWT一直就是用的OpenGL ES 2.0模式在渲染。

接下来,我们添加一个新的类DirectedGame来管理场景使用过渡的情况(替换掉Libgdx的Game类):

package com.packtpub.libgdx.canyonbunny.screens;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.packtpub.libgdx.canyonbunny.screens.transitions.ScreenTransition;

public abstract class DirectedGame implements ApplicationListener {
    private boolean init;
    private AbstractGameScreen currScreen;
    private AbstractGameScreen nextScreen;
    private FrameBuffer currFbo;
    private FrameBuffer nextFbo;
    private SpriteBatch batch;
    private float t;
    private ScreenTransition screenTransition;

    public void setScreen(AbstractGameScreen screen) {
        setScreen(screen, null);
    }

    public void setScreen(AbstractGameScreen screen,
            ScreenTransition screenTransition) {
        int w = Gdx.graphics.getWidth();
        int h = Gdx.graphics.getHeight();
        if (!init) {
            currFbo = new FrameBuffer(Format.RGB888, w, h, false);
            nextFbo = new FrameBuffer(Format.RGB888, w, h, false);
            batch = new SpriteBatch();
            init = true;
        }
        // start new transition
        nextScreen = screen;
        nextScreen.show(); // activate next screen
        nextScreen.resize(w, h);
        nextScreen.render(0); // let screen update() once
        if (currScreen != null)
            currScreen.pause();
        nextScreen.pause();
        Gdx.input.setInputProcessor(null); // disable input
        this.screenTransition = screenTransition;
        t = 0;
    }
}

重写ApplicationListener接口的方法:

@Override
    public void render() {
        // get delta time and ensure an upper limit of one 60th second
        float deltaTime = Math.min(Gdx.graphics.getDeltaTime(), 1.0f / 60.0f);
        if (nextScreen == null) {
            // no ongoing transition
            if (currScreen != null)
                currScreen.render(deltaTime);
        } else {
            // ongoing transition
            float duration = 0;
            if (screenTransition != null)
                duration = screenTransition.getDuration();
            // update progress of ongoing transition
            t = Math.min(t + deltaTime, duration);
            if (screenTransition == null || t >= duration) {
                // no transition effect set or transition has just finished
                if (currScreen != null)
                    currScreen.hide();
                nextScreen.resume();
                // enable input for next screen
                Gdx.input.setInputProcessor(nextScreen.getInputProcessor());
                // switch screens
                currScreen = nextScreen;
                nextScreen = null;
                screenTransition = null;
            } else {
                // render screens to FBOs
                currFbo.begin();
                if (currScreen != null)
                    currScreen.render(deltaTime);
                currFbo.end();
                nextFbo.begin();
                nextScreen.render(deltaTime);
                nextFbo.end();
                // render transition effect to screen
                float alpha = t / duration;
                screenTransition.render(batch, currFbo.getColorBufferTexture(),
                        nextFbo.getColorBufferTexture(), alpha);
            }
        }
    }

    @Override
    public void resize(int width, int height) {
        if (currScreen != null)
            currScreen.resize(width, height);
        if (nextScreen != null)
            nextScreen.resize(width, height);
    }

    @Override
    public void pause() {
        if (currScreen != null)
            currScreen.pause();
    }

    @Override
    public void resume() {
        if (currScreen != null)
            currScreen.resume();
    }

    @Override
    public void dispose() {
        if (currScreen != null)
            currScreen.hide();
        if (nextScreen != null)
            nextScreen.hide();
        if (init) {
            currFbo.dispose();
            currScreen = null;
            nextFbo.dispose();
            nextScreen = null;
            batch.dispose();
            init = false;
        }
    }

最后,我们要把先前用的Game类的地方,替换成DirectedGame.

首先修改AbstractGameScreen:

    protected DirectedGame game;

    public AbstractGameScreen(DirectedGame game) {
        this.game = game;
    }

    public abstract InputProcessor getInputProcessor();

然后修改CanyonBunnyMain然它继承DirectedGame。

还有MenuScreen的构造函数改成public MenuScreen (DirectedGame game),同时添加:

    @Override
    public void show() {
        stage = new Stage();
        rebuildStage();
    }
    @Override
    public InputProcessor getInputProcessor() {
        return stage;
    }

同样,GameScreen相应修改构造函数参数类型Game为DirectedGame,还有添加:

    @Override
    public InputProcessor getInputProcessor() {
        return worldController;
    }

最后,修改worldcontroller里的game为DirectedGame,移除init中的InputProcessor:

private void init () {
cameraHelper = new CameraHelper();
lives = Constants.LIVES_START;
livesVisual = lives;
timeLeftGameOverDelay = 0;
initLevel();
}

应用的代码已经修改完成了,但是我们的转场特效现在还是空的,接下来一步步实现它。

这里有必要普及一下理论知识,转场特效的核心就是插值算法,Libgdx已经实现了很多线性和非线性的插值算法,我们来看看这些算法图:

通俗的讲,这些图就相当于每一个供查询的表,用户提供一个阿尔法值(x轴),通过表就得到一个结果值(y轴)。

在其他的游戏引擎中,比如Cocos2D引擎,已经封装了很多转场效果供开发者调用,比如Cocos里的FadeIn和FadeOut,就是对应于fade图的阿尔法的取值范围0-0.5 和 0.5-1。

在Libgdx中,这些特效都是不固定的,开发者可以自由组合。通常用法是这样的:

float alpha = 0.25f;
float interpolatedValue = Interpolation.elastic.apply(alpha);

这里的阿尔法除了可以理解为参数以外,还可以理解为是整个动作进行的百分比。

下面我们来创建fade, slide 和 slice效果。

fade就是当前场景从不透明到完全透明,同时新场景从透明到完全不透明。

创建类ScreenTransitionFade:

package com.packtpub.libgdx.canyonbunny.screens.transitions;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation;

public class ScreenTransitionFade implements ScreenTransition {
    private static final ScreenTransitionFade instance = new ScreenTransitionFade();
    private float duration;

    public static ScreenTransitionFade init(float duration) {
        instance.duration = duration;
        return instance;
    }

    @Override
    public float getDuration() {
        return duration;
    }

    @Override
    public void render(SpriteBatch batch, Texture currScreen,
            Texture nextScreen, float alpha) {
        float w = currScreen.getWidth();
        float h = currScreen.getHeight();
        alpha = Interpolation.fade.apply(alpha);
        Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        batch.begin();
        batch.setColor(1, 1, 1, 1);
        batch.draw(currScreen, 0, 0, 0, 0, w, h, 1, 1, 0, 0, 0,
                currScreen.getWidth(), currScreen.getHeight(), false, true);
        batch.setColor(1, 1, 1, alpha);
        batch.draw(nextScreen, 0, 0, 0, 0, w, h, 1, 1, 0, 0, 0,
                nextScreen.getWidth(), nextScreen.getHeight(), false, true);
        batch.end();
    }
}

现在,我们把play按钮的clicked代码修改一下,用上fade的效果:

ScreenTransition transition = ScreenTransitionFade.init(0.75f);
game.setScreen(new GameScreen(game), transition);

创建类ScreenTransitionSlide:

package com.packtpub.libgdx.canyonbunny.screens.transitions;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation;

public class ScreenTransitionSlide implements ScreenTransition {
    public static final int LEFT = 1;
    public static final int RIGHT = 2;
    public static final int UP = 3;
    public static final int DOWN = 4;
    private static final ScreenTransitionSlide instance = new ScreenTransitionSlide();
    private float duration;
    private int direction;
    private boolean slideOut;
    private Interpolation easing;

    public static ScreenTransitionSlide init(float duration, int direction,
            boolean slideOut, Interpolation easing) {
        instance.duration = duration;
        instance.direction = direction;
        instance.slideOut = slideOut;
        instance.easing = easing;
        return instance;
    }

    @Override
    public float getDuration() {
        return duration;
    }

    @Override
    public void render(SpriteBatch batch, Texture currScreen,
            Texture nextScreen, float alpha) {
        float w = currScreen.getWidth();
        float h = currScreen.getHeight();
        float x = 0;
        float y = 0;
        if (easing != null)
            alpha = easing.apply(alpha);
        // calculate position offset
        switch (direction) {
        case LEFT:
            x = -w * alpha;
            if (!slideOut)
                x += w;
            break;
        case RIGHT:
            x = w * alpha;
            if (!slideOut)
                x -= w;
            break;
        case UP:
            y = h * alpha;
            if (!slideOut)
                y -= h;
            break;
        case DOWN:
            y = -h * alpha;
            if (!slideOut)
                y += h;
            break;
        }
        // drawing order depends on slide type ('in' or 'out')
        Texture texBottom = slideOut ? nextScreen : currScreen;
        Texture texTop = slideOut ? currScreen : nextScreen;
        // finally, draw both screens
        Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        batch.begin();
        batch.draw(texBottom, 0, 0, 0, 0, w, h, 1, 1, 0, 0, 0,
                currScreen.getWidth(), currScreen.getHeight(), false, true);
        batch.draw(texTop, x, y, 0, 0, w, h, 1, 1, 0, 0, 0,
                nextScreen.getWidth(), nextScreen.getHeight(), false, true);
        batch.end();
    }
}

然后在worldcontroller的backmenu里使用这个效果:

    private void backToMenu() {
        // switch to menu screen
        ScreenTransition transition = ScreenTransitionSlide.init(0.75f,
                ScreenTransitionSlide.DOWN, false, Interpolation.bounceOut);
        game.setScreen(new MenuScreen(game), transition);
    }

创建类ScreenTransitionSlice:

package com.packtpub.libgdx.canyonbunny.screens.transitions;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.utils.Array;

public class ScreenTransitionSlice implements ScreenTransition {
    public static final int UP = 1;
    public static final int DOWN = 2;
    public static final int UP_DOWN = 3;
    private static final ScreenTransitionSlice instance = new ScreenTransitionSlice();
    private float duration;
    private int direction;
    private Interpolation easing;
    private Array<Integer> sliceIndex = new Array<Integer>();

    public static ScreenTransitionSlice init(float duration, int direction,
            int numSlices, Interpolation easing) {
        instance.duration = duration;
        instance.direction = direction;
        instance.easing = easing;
        // create shuffled list of slice indices which determines
        // the order of slice animation
        instance.sliceIndex.clear();
        for (int i = 0; i < numSlices; i++)
            instance.sliceIndex.add(i);
        instance.sliceIndex.shuffle();
        return instance;
    }

    @Override
    public float getDuration() {
        return duration;
    }

    @Override
    public void render(SpriteBatch batch, Texture currScreen,
            Texture nextScreen, float alpha) {
        float w = currScreen.getWidth();
        float h = currScreen.getHeight();
        float x = 0;
        float y = 0;
        int sliceWidth = (int) (w / sliceIndex.size);
        Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        batch.begin();
        batch.draw(currScreen, 0, 0, 0, 0, w, h, 1, 1, 0, 0, 0,
                currScreen.getWidth(), currScreen.getHeight(), false, true);
        if (easing != null)
            alpha = easing.apply(alpha);
        for (int i = 0; i < sliceIndex.size; i++) {
            // current slice/column
            x = i * sliceWidth;
            // vertical displacement using randomized
            // list of slice indices
            float offsetY = h
                    * (1 + sliceIndex.get(i) / (float) sliceIndex.size);
            switch (direction) {
            case UP:
                y = -offsetY + offsetY * alpha;
                break;
            case DOWN:
                y = offsetY - offsetY * alpha;
                break;
            case UP_DOWN:
                if (i % 2 == 0) {
                    y = -offsetY + offsetY * alpha;
                } else {
                    y = offsetY - offsetY * alpha;
                }
                break;
            }
            batch.draw(nextScreen, x, y, 0, 0, sliceWidth, h, 1, 1, 0, i
                    * sliceWidth, 0, sliceWidth, nextScreen.getHeight(), false,
                    true);
        }
        batch.end();
    }
}

在CanyonBunnyMain中使用这个特效:

@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
ScreenTransition transition = ScreenTransitionSlice.init(2,
ScreenTransitionSlice.UP_DOWN, 10, Interpolation.pow5Out);
setScreen(new MenuScreen(this), transition);
}

ok,增加了转场效果之后,游戏是不是漂亮了许多?

在下一章,我们将添加音乐和音效。

原文地址:https://www.cnblogs.com/mignet/p/libgdx_game_development_09.html