本篇大约有5810个字,阅读预计需要7.26分钟
暂停也是一个游戏的必要功能了,本文研究了Libgdx实现游戏暂停
例子以桌面端游戏实现讲解为主,至于移动端,可能之后会进行补充...
本文最终实现的就是
按下esc暂停,之后会出现一个界面提示,表示当前已经暂停
重新按下esc,则返回继续游戏
本篇稍微学习了下libgdx里的输入事件监听
最初方案1
最初看的教程是,通过一个boolean变量来控制render渲染,这里我们以上文例子Libgdx游戏开发(5)——碰撞反弹的简单实践-Stars-One的杂货小窝borroweachinforefunddoll代码为例,增加一个暂停功能
注:下面贴的是全部代码,发布各位自行运行,但后续代码例子只看
CircleBallTest
这个类,为了方便阅读其他类就不再贴出了...
import com.badlogic.gdx.ApplicationAdapter import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.GL20 import com.badlogic.gdx.graphics.glutils.ShapeRenderer class CircleBallTest : ApplicationAdapter() { lateinit var shape: ShapeRenderer val ball by lazy { Ball() } val line by lazy { MyBan() } override fun create() { shape = ShapeRenderer() } //增加一个变量标识 var isPause = false override fun render() { //监听按下esc键,修改标识 if (Gdx.input.isKeyPressed(Input.Keys.ESCAPE)) { isPause = !isPause } if (isPause) { //如果是暂停,则不再进行绘制 return } Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) line.control() ball.gundon() line.draw(shape) ball.draw(shape) ball.checkFz() //检测碰撞到数横条 ball.checkLineP(line) } } class MyBan { var width = 200f var height = 10f var x = 0f var y = height fun draw(shape: ShapeRenderer) { shape.begin(ShapeRenderer.ShapeType.Filled) //这里注意: x,y是指矩形的左上角 shape.rect(x, height, width, height) shape.end() } fun control() { if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { x -= 200 * Gdx.graphics.deltaTime } if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { x += 200 * Gdx.graphics.deltaTime } //这里屏蔽y坐标改变,只给控制左右移动 return if (Gdx.input.isKeyPressed(Input.Keys.UP)) { y += 200 * Gdx.graphics.deltaTime } if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { y -= 200 * Gdx.graphics.deltaTime } } } class Ball { var size = 5f var x = 50f var y = 50f var speedX = 5f var speedY = 5f fun checkLineP(myBan: MyBan) { if (y - size <= myBan.y) { speedY = speedY * -1 } } fun gundon() { x += speedX y += speedY } fun draw(shape: ShapeRenderer) { shape.begin(ShapeRenderer.ShapeType.Filled) shape.circle(x, y, size) shape.end() } fun checkFz() { //到达右边缘,x变反 if (x + size >= Gdx.graphics.width) { speedX = speedX * -1 } //到达下边缘,y变反 //todo 这个是判输条件! if (y - size <= 0) { speedY = speedY * -1 } //到达上边缘,y变反 if (y + size >= Gdx.graphics.height) { speedY = speedY * -1 } //到达左边缘,x变反 if (x - size <= 0) { speedX = speedX * -1 } } }
效果:
可能上面的效果看得不明显,我已经按了3次esc,但是发现好像没暂停,什么原因导致的?
原因很简单,render()
方法是每帧进行渲染的,所以导致我们的监听会执行多次,我们加个日志打印就能发现端倪,如下图:
按下了一次,但由于每帧都会渲染,所以触发了多次
优化方案2 - 事件拦截器监听按键
经过了一番百度和GPT询问,得知Libgdx里有一个输入事件的拦截器接口InputProcessor
为了方便我们不必重写每个此接口的每个方法,我们可以使用InputAdapter
(这个是InputProcessor
接口的空实现类),之后重写需要的方法即可
我们以上面需求,实现监听esc键的监听,代码如下:
class InputP:InputAdapter(){ //暂停标志 var isPause = false override fun keyDown(keycode: Int): Boolean { if (keycode == Input.Keys.ESCAPE) { //按下esc按键则修改状态 isPause=!isPause } //ps:如果想监听android上的返回键,则可以使用Input.Keys.BACK,不过得先调用 return true } }
接着通过Gdx.input.inputProcessor
(我这里是kotlin写法,java的话则是个setinputProcessor*()方法)进行设置拦截事件的拦截
class CircleBallTest : ApplicationAdapter() { lateinit var shape: ShapeRenderer val ball by lazy { Ball() } val line by lazy { MyBan() } //懒加载创建拦截器对象 val inputPro by lazy { InputP() } override fun create() { shape = ShapeRenderer() //开始之前就设置拦截器 Gdx.input.inputProcessor = inputPro } override fun render() { //通过拦截器里的标志判断当前是否暂停绘制 if (inputPro.isPause) { } else { Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) line.draw(shape) ball.draw(shape) line.control() ball.gundon() ball.checkFz() //检测碰撞到数横条 ball.checkLineP(line) } } }
效果:
从动图效果来看,暂停功能是实现了,但是出现了闪动的问题
这里虽然具体原理不清楚,但是根据之前做过Android动态壁纸的研究,知道这种底层还是使用OpenGl绘制,所以直接猜测OpenGl渲染缓存中有上一帧数据,导致了此问题
优化方案3 - 暂停状态重绘
根据上面的原因,所以有下解决思路:
暂停状态下,重新绘制当前的UI,但不改变物体的x,y坐标
这里就提到了上章节说的,为什么要将绘制和坐标逻辑计算分开不同方法来写的原因了
如下代码(只贴核心代码):
override fun render() { if (inputPro.isPause) { //这里重新绘制 Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) line.draw(shape) ball.draw(shape) return } Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) line.draw(shape) ball.draw(shape) line.control() ball.gundon() ball.checkFz() //检测碰撞到数横条 ball.checkLineP(line) }
效果如下图:
但个人感觉这种方案,在暂停了但仍然会不停的绘制,感觉有些性能浪费,于是有了下面的方案4
优化方案4
再回到问题上来,因为是上一帧和当前帧切换导致的问题,所以我们将上一帧和当前帧整成一样,绘制的时候就不会出现闪动的状态了吧,得到一个新的解决思路:
每次进入暂停状态后,绘制2遍帧数据,保证上一帧和当前帧相同,之后即可跳过绘制过程,由于前2帧一直是相同的,所以就不会出现抖动的效果,即则完成我们需要的效果和优化的效果
class InputP : InputAdapter() { var isPause = false override fun keyDown(keycode: Int): Boolean { if (keycode == Input.Keys.ESCAPE) { isPause = !isPause } //如果不是暂停状态,则重置 if (isPause.not()) { count=0 } return true } var count = 0 fun handlePase(drawAction: () -> Unit) { //这里保证绘制完2帧 if (count > 1) { return } drawAction.invoke() count++ } } class CircleBallTest : ApplicationAdapter() { lateinit var shape: ShapeRenderer val ball by lazy { Ball() } val line by lazy { MyBan() } override fun create() { shape = ShapeRenderer() Gdx.input.inputProcessor = inputPro } val inputPro by lazy { InputP() } override fun render() { if (inputPro.isPause) { inputPro.handlePase { draw() } return } draw() line.control() ball.gundon() ball.checkFz() //检测碰撞到数横条 ball.checkLineP(line) } private fun draw() { Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) line.draw(shape) ball.draw(shape) } }
由于效果与上面相同,这里就不上图了
优化方案5
上面已经完成实现暂停功能了,现在我们在上面基础上个加个暂停文字提示(之前章节已经讲过如何绘制文字了),这里简单起见,我们直接显示pause单词
class CircleBallTest : ApplicationAdapter() { lateinit var shape: ShapeRenderer val ball by lazy { Ball() } val line by lazy { MyBan() } val batch: SpriteBatch by lazy { SpriteBatch() } val font: BitmapFont by lazy { BitmapFont() } override fun create() { shape = ShapeRenderer() Gdx.input.inputProcessor = inputPro } val inputPro by lazy { InputP() } override fun render() { if (inputPro.isPause) { inputPro.handlePase { draw() //绘制暂停提示 Gdx.gl.glClearColor(0f, 0f, 0f, 0.8f); // 设置清屏颜色为透明度80%的黑色 batch.begin() font.draw(batch, "Pause", 100f, 150f) batch.end() } return } draw() line.control() ball.gundon() ball.checkFz() //检测碰撞到数横条 ball.checkLineP(line) } private fun draw() { Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT) line.draw(shape) ball.draw(shape) } }
效果如下:
补充 - 监听android手机的返回键
如果想要监听android手机的返回键,则需要先设置Gdx.input.setCatchKey(Input.Keys.BACK, true)
,之后和上述一样监听keycode==Keys.BACK
即可实现,如下图代码示例
这里就不放演示动图了,实际测试效果按下返回键即可暂停,但好像分辨率没有兼容,导致小球特别小,后续优化的时候再研究了...