本章主要讲解场景过渡效果的使用。这里将用到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,增加了转场效果之后,游戏是不是漂亮了许多?
在下一章,我们将添加音乐和音效。