• Libgdx游戏开发(6)——游戏暂停


    本文为作者原创,允许转载,不过请在文章开头明显处注明链接和出处!!! 谢谢配合~
    作者:stars-one
    链接:https://www.cnblogs.com/stars-one/p/18270044

    本篇大约有5811个字,阅读预计需要7.26分钟


    原文: Libgdx游戏开发(6)——游戏暂停-Stars-One的杂货小窝

    暂停也是一个游戏的必要功能了,本文研究了Libgdx实现游戏暂停

    例子以桌面端游戏实现讲解为主,至于移动端,可能之后会进行补充...

    本文最终实现的就是

    按下esc暂停,之后会出现一个界面提示,表示当前已经暂停

    重新按下esc,则返回继续游戏

    本篇稍微学习了下libgdx里的输入事件监听

    最初方案1

    最初看的教程是,通过一个boolean变量来控制render渲染,这里我们以上文例子Libgdx游戏开发(5)——碰撞反弹的简单实践-Stars-One的杂货小窝代码为例,增加一个暂停功能

    注:下面贴的是全部代码,发布各位自行运行,但后续代码例子只看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即可实现,如下图代码示例

    这里就不放演示动图了,实际测试效果按下返回键即可暂停,但好像分辨率没有兼容,导致小球特别小,后续优化的时候再研究了...

    参考

  • 相关阅读:
    Matplotlib设置限制制作
    目录和文件管理
    SAP 电商云 Spartacus UI 的 Product Category Navigation UI 实现
    Pytorch深度学习模型的指标测试【1】(如:模型大小、模型操作量)
    lvgl lv_obj_align_to函数
    java+ssh+mysql毕业生实习招聘管理系统
    【C语言】多进程服务器
    游戏公会在去中心化游戏中的未来
    什么是闭包,应用场景是什么?
    QT用户登录注册,数据库实现
  • 原文地址:https://www.cnblogs.com/stars-one/p/18270044