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

libgdx · 浏览次数 : 0

小编点评

本文主要介绍了在Libgdx游戏中实现游戏暂停功能的方法。首先,通过研究官方文档和示例代码,了解如何在桌面端实现游戏暂停。然后,针对移动端,作者会在后续章节中进行补充。 1. **学习背景**:本文以Libgdx游戏开发(5)——碰撞反弹的简单实践——Stars-One的杂货小窝为例,增加了暂停功能,并简要学习了libgdx中的输入事件监听。 2. **实现方法**:作者通过增加一个布尔变量`isPause`来控制游戏是否暂停,并在渲染循环中加入了对`ESCAPE`键的监听。当按下`ESCAPE`键时,会根据当前状态更新`isPause`的值,并根据值决定是否停止渲染。 3. **优化方案**:为了减少闪动和提高性能,作者提出了几种优化方案。其中包括使用InputProcessor接口简化监听过程、重绘暂停状态下的UI以及优化绘制过程避免闪动。 4. **添加文字提示**:最后,作者为游戏添加了一个暂停文字提示,并展示了如何在渲染循环中加入文字绘制。 5. **监听安卓返回键**:如果需要监听安卓手机的返回键,需要在游戏界面上设置相应的键盘监听,并在处理函数中判断是否为返回键并按需处理。 通过以上步骤,作者成功地为Libgdx游戏实现了游戏暂停功能,并提供了多种优化方案供读者参考。

正文

原文: 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即可实现,如下图代码示例

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

参考

与Libgdx游戏开发(6)——游戏暂停相似的内容:

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

原文: Libgdx游戏开发(6)——游戏暂停-Stars-One的杂货小窝 暂停也是一个游戏的必要功能了,本文研究了Libgdx实现游戏暂停 例子以桌面端游戏实现讲解为主,至于移动端,可能之后会进行补充... 本文最终实现的就是 按下esc暂停,之后会出现一个界面提示,表示当前已经暂停 重新按下e

Libgdx游戏开发(7)——开始游戏界面实现

原文: Libgdx游戏开发(7)——开始游戏界面实现-Stars-One的杂货小窝 上篇文章也是讲解了如何实现暂停,但实际上,上篇的做法可能不够优雅 因为暂停和游戏界面我们可以分成2个Screen对象,这样只需要监听键盘输入,更改显示不同的Screen对象即可 本文的实现目标: 使用Screen来

Libgdx游戏开发(5)——碰撞反弹的简单实践

原文: Libgdx游戏开发(5)——碰撞反弹的简单实践-Stars-One的杂货小窝 本篇简单以一个小球运动,一步步实现碰撞反弹的效果 本文代码示例以kotlin为主,且需要有一定的Libgdx入门基础 注:下面动态图片看着有些卡顿,是录制的问题,实际上运行时很流畅的 水平滚动 简单起见,我们通过

Libgdx游戏开发(4)——显示中文文字

原文: Libgdx游戏开发(4)——显示中文文字-Stars-One的杂货小窝 本文代码示例采用kotlin代码进行讲解,且需要有libgdx入门基础 这里主要介绍关于在Libgdx显示文字的2种方法 2种方法优缺点 BitmapFont 优势: 易于操作和使用,简单快速实现文本渲染。 资源消耗相

Libgdx游戏开发(3)——通过柏林噪音算法地图随机地形

原文: Libgdx游戏开发(3)——通过柏林噪音算法地图随机地形-Stars-One的杂货小窝 在B站刷到了随机地图生成的视频,随手学习下并做下记录 注: 本篇使用javafx应用作演示,算是了解这个算法的使用,后续会再出篇libgdx生成地图的示例 说明 抛开算法实现,首先认知柏林噪音算法 一般