You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Kotlin与LibGDX的协程Bug排查求助

Kotlin+LibGDX协程呼吸缩放效果失效问题解决

问题背景

用Kotlin结合LibGDX开发游戏,之前基于ApplicationAdapter单类测试时一切正常,但改用Game类架构后,通过协程实现的4个盒子呼吸缩放效果初期运行正常,一段时间后会失效,触摸功能不受影响。尝试过修改dispose()方法和相机配置,未能解决问题。

问题根源

  1. 协程重复启动:在render()方法中每次调用extendSize(),而该方法每次都会启动一个新的GlobalScope协程,导致大量协程并发修改form中的Rectangle尺寸,竞态条件让逻辑彻底混乱,最终效果失效。
  2. 生命周期未绑定:GlobalScope协程没有和Screen的生命周期绑定,且无有效管理,多个协程同时操作同一组尺寸变量,数值被反复覆盖导致异常。
  3. 纹理内存泄漏:触摸切换纹理时未释放旧纹理,长期运行会占用大量内存,间接影响协程运行稳定性。

修复方案

核心改动点

  • 控制协程仅启动一次,避免重复创建
  • 用Job管理协程生命周期,在屏幕切换/销毁时取消协程
  • 修复纹理泄漏问题,替换纹理时释放旧资源
  • 优化协程内逻辑,加入运行状态检查

修改后代码示例

调整后的escollir_color类

package com.la_rateta.game

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Screen
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.GlyphLayout
import com.badlogic.gdx.math.Rectangle
import com.badlogic.gdx.math.Vector3
import kotlinx.coroutines.*

class escollir_color(game: game) : Screen {

    private lateinit var gameInstance: game
    private var camera: OrthographicCamera = OrthographicCamera()
    private var image: ArrayList<Texture?> = ArrayList()
    private var form: ArrayList<Rectangle> = ArrayList()
    private var enunciats: ArrayList<String> = ArrayList()
    private var random: Int? = null
    private var layout: GlyphLayout? = null
    // 用Job管理协程生命周期
    private var scaleJob: Job? = null

    init {
        gameInstance = game
        camera.setToOrtho(false, 800f, 400f)
        layout = GlyphLayout()
        addColors()
        randomText()
        // 初始化时仅启动一次缩放协程
        startScaleCoroutine()
    }

    override fun render(delta: Float) {
        camera.update()
        gameInstance.batch!!.setProjectionMatrix(camera.combined)

        // 每次渲染先清屏,避免画面残留
        Gdx.gl.glClearColor(0f, 0f, 0f, 1f)
        Gdx.gl.glClear(Gdx.gl.GL_COLOR_BUFFER_BIT)

        gameInstance.batch!!.begin()
        gameInstance.font!!.draw(gameInstance.batch, enunciats[random!!], (800 - layout!!.width)/2, 370f)
        for (i in image.indices) {
            gameInstance.batch!!.draw(image[i], form[i].x, form[i].y, form[i].width, form[i].height)
        }
        gameInstance.batch!!.end()

        if (Gdx.input.isTouched()) {
            touching()
        }
    }

    private fun startScaleCoroutine() {
        scaleJob = GlobalScope.launch(Dispatchers.Main) {
            // 同时检查全局状态和协程自身活跃状态
            while (gameInstance.isDisplaying && isActive) {
                // 重置尺寸到初始值
                for (i in 0 until form.size - 1) {
                    form[i].width = 80f
                    form[i].height = 80f
                }
                // 放大阶段
                repeat(15) {
                    if (!gameInstance.isDisplaying || !isActive) break
                    for (i in 0 until form.size - 1) {
                        form[i].width += 1f
                        form[i].height += 1f
                    }
                    delay(100)
                }
                // 缩小阶段
                repeat(15) {
                    if (!gameInstance.isDisplaying || !isActive) break
                    for (i in 0 until form.size - 1) {
                        form[i].width -= 1f
                        form[i].height -= 1f
                    }
                    delay(100)
                }
            }
        }
    }

    private fun addColors() {
        var display = Rectangle().apply {
            x = 100f; y = 200f; width = 80f; height = 80f
        }
        image.add(Texture("yellow_color.jpg"))
        form.add(display)

        display = Rectangle().apply {
            x = 210f; y = 200f; width = 80f; height = 80f
        }
        image.add(Texture("green_color.png"))
        form.add(display)

        display = Rectangle().apply {
            x = 100f; y = 90f; width = 80f; height = 80f
        }
        image.add(Texture("pink_color.jpg"))
        form.add(display)

        display = Rectangle().apply {
            x = 210f; y = 90f; width = 80f; height = 80f
        }
        image.add(Texture("blue_color.jpg"))
        form.add(display)

        display = Rectangle().apply {
            x = 500f; y = 90f; width = 200f; height = 200f
        }
        image.add(Texture("ribbon.png"))
        form.add(display)
    }

    private fun randomText() {
        enunciats.apply {
            add("El llaç és de color groc")
            add("El llaç és de color verd")
            add("El llaç és de color rosa")
            add("El llaç és de color blau")
        }
        random = (0..3).random()
        layout!!.setText(gameInstance.font, enunciats[random!!])
    }

    private fun touching() {
        gameInstance.touch.set(Gdx.input.getX().toFloat(), Gdx.input.getY().toFloat(), 0f)
        camera.unproject(gameInstance.touch)

        for (i in form.indices) {
            if (form[i].contains(gameInstance.touch.x, gameInstance.touch.y)) {
                when (i) {
                    0 -> {
                        image[4]?.dispose() // 释放旧纹理,避免内存泄漏
                        image[4] = Texture("yellow_ribbon.png")
                    }
                    1 -> {
                        image[4]?.dispose()
                        image[4] = Texture("green_ribbon.png")
                    }
                    2 -> {
                        image[4]?.dispose()
                        image[4] = Texture("pink_ribbon.png")
                    }
                    3 -> {
                        image[4]?.dispose()
                        image[4] = Texture("blue_ribbon.png")
                    }
                }
            }
        }
    }

    override fun show() {}

    override fun resize(width: Int, height: Int) {}

    override fun pause() {}

    override fun resume() {}

    override fun hide() {
        // 切换屏幕时取消协程
        scaleJob?.cancel()
    }

    override fun dispose() {
        // 释放所有纹理资源
        image.forEach { it?.dispose() }
        scaleJob?.cancel()
        // Game类负责全局资源销毁,这里仅释放当前Screen的专属资源
        gameInstance.font_generator?.dispose()
    }
}

调整后的game类

class game : Game() {

    var batch: SpriteBatch? = null
    var font_generator: FreeTypeFontGenerator? = null
    val font_parameter = FreeTypeFontGenerator.FreeTypeFontParameter()
    var font: BitmapFont? = null
    val touch = Vector3()
    var isDisplaying = true

    override fun create() {
        batch = SpriteBatch()
        setFontConfig()
        font_generator = FreeTypeFontGenerator(Gdx.files.internal("fonts/indie_flower.ttf"))
        font = font_generator?.generateFont(font_parameter)
        setScreen(escollir_color(this))
    }

    override fun render() {
        super.render()
    }

    override fun dispose() {
        batch?.dispose()
        font?.dispose()
        font_generator?.dispose() // 补全字体生成器的资源释放
    }

    private fun setFontConfig() {
        font_parameter.size = 60
        font_parameter.shadowOffsetX = 2
        font_parameter.shadowOffsetY = 2
        font_parameter.shadowColor = Color(0f, 0f, 0f, 1f)
        font_parameter.color = Color.WHITE // 简化写法,等价于原Color(255f,255f,255f,1f)
    }
}

额外优化建议

  • 尽量避免!!非空断言,可提前初始化变量或用?.安全调用,降低空指针风险
  • 协程优先使用绑定生命周期的Scope(比如自定义CoroutineScope),替代GlobalScope
  • 缩放逻辑可改用基于delta时间的帧更新方式,更贴合LibGDX的渲染机制,避免固定延迟带来的帧率依赖问题

内容的提问来源于stack exchange,提问作者Joansanbib

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 02:57:33