基于Kotlin与LibGDX的协程Bug排查求助
Kotlin+LibGDX协程呼吸缩放效果失效问题解决
问题背景
用Kotlin结合LibGDX开发游戏,之前基于ApplicationAdapter单类测试时一切正常,但改用Game类架构后,通过协程实现的4个盒子呼吸缩放效果初期运行正常,一段时间后会失效,触摸功能不受影响。尝试过修改dispose()方法和相机配置,未能解决问题。
问题根源
- 协程重复启动:在
render()方法中每次调用extendSize(),而该方法每次都会启动一个新的GlobalScope协程,导致大量协程并发修改form中的Rectangle尺寸,竞态条件让逻辑彻底混乱,最终效果失效。 - 生命周期未绑定:
GlobalScope协程没有和Screen的生命周期绑定,且无有效管理,多个协程同时操作同一组尺寸变量,数值被反复覆盖导致异常。 - 纹理内存泄漏:触摸切换纹理时未释放旧纹理,长期运行会占用大量内存,间接影响协程运行稳定性。
修复方案
核心改动点
- 控制协程仅启动一次,避免重复创建
- 用
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
相关产品推荐
相关产品推荐

