Hi, I’m pretty new to Compose (haha, I mean trying...
# compose-desktop
o
Hi, I’m pretty new to Compose (haha, I mean trying to really do something with it), and I want to use it for simple game UI we are doing with my kid. It’s not even 2D game, just some controls and images reflecting current state, kinda like incremental/idle games, if you know. While I understand unidirectional approach, incremental computations and such, I can’t find a simple way to do a game loop. Basically, I want it to call some
Update(dt)
function frequently passing time passed since last call, which would update game mutable state and then I want UI to only touch things that actually depend on the change. I’ve searched the net and only found some clocks and Dino game, but they are just animating things directly, not doing this intermediate game state thing. Am I missing something? How I’d go with it?
a
Long time no see! I do not think that game graphics is a good idea to use any declarative frameworks. But if you want to try, you basically use any shceduler and update the state you put into
mutableState
.
o
note that Compose in a sense has its own animation/game loop, so just update the state and everything else will work automagically 🙂. And as one could use coroutines, it could be as simple as
Copy code
fun main() {
    val state = mutableStateOf(0)
    GlobalScope.launch {
        while (true) {
            delay(100)
            state.value++
        }
    }
    Window {
        Text(state.value.toString())
    }
}
👍 2
i
has its own animation/game loop
It is better to use it instead of your own if we want precise animations. Using
LaunchedEffect
and `withFrameNanos`:
Copy code
import androidx.compose.desktop.Window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.dispatch.withFrameNanos
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

fun main() = Window {
    val game = remember { Game() }

    LaunchedEffect(Unit) {
        while (true) {
            withFrameNanos {
                game.update(it)
            }
        }
    }

    Box(Modifier.offset(game.position.dp)) {
        Box(Modifier.size(20.dp).background(Color.Red))
    }
}

class Game {
    private val velocityPixelsPerSecond = 100f
    private var previousTimeNanos: Long = Long.MAX_VALUE

    var position by mutableStateOf(0f)
        private set

    fun update(nanos: Long) {
        val dt = (nanos - previousTimeNanos).coerceAtLeast(0)
        previousTimeNanos = nanos

        position += (dt / 1E9 * velocityPixelsPerSecond).toFloat()
    }
}
🎉 3
👍 2
o
Oh, precious, thanks @Igor Demin!
o
Copy code
import androidx.compose.desktop.Window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.dispatch.withFrameNanos
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
    Window {
        Column {
            val game = remember { Game() }
            val effect = remember { mutableStateOf(false) }
            val delay = remember { mutableStateOf(10) }
            Text("Now using ${if (effect.value) "Effect" else "Timer"}")
            Button(onClick = { effect.value = !effect.value }) {
                Text("Toggle to ${if (effect.value) "Timer" else "Effect"}")
            }
            TextField(value = delay.value.toString(), onValueChange = {
                    newValue -> delay.value = newValue.toIntOrNull() ?: 10
            })
            if (effect.value) {
                LaunchedEffect(Unit) {
                    while (effect.value) {
                        withFrameNanos {
                            game.update(it)
                        }
                    }
                }
            } else {
                remember {
                    GlobalScope.launch(Dispatchers.Main) {
                        while (!effect.value) {
                            game.update(System.nanoTime())
                            delay(delay.value.toLong())
                        }
                    }
                }
            }
            Box(Modifier.offset(game.position.dp)) {
                Box(Modifier.size(20.dp).background(Color.Red))
            }
        }
    }
}
class Game {
    private val velocityPixelsPerSecond = 100f
    private var previousTimeNanos: Long = Long.MAX_VALUE
    var position by mutableStateOf(0f)
        private set
    fun update(nanos: Long) {
        val dt = (nanos - previousTimeNanos).coerceAtLeast(0)
        previousTimeNanos = nanos
        position += (dt / 1E9 * velocityPixelsPerSecond).toFloat()
        if (position > 500) position = 0f
    }
}
o
Thanks, I don’t need to be frame-precise, at least not yet. But it may help in the future.
i
Note that there are the issues with built-in loop (Compose can sometimes skip frames), but we will fix this soon.
👍 1
c
@orangy you might want to check out #compose to see the flappy bird game someone made
o
Yeah, thanks! I will when I stuck next time 🙂
m
@Igor Demin Thanks, that’s exactly the example I was looking for in my question here https://kotlinlang.slack.com/archives/C01D6HTPATV/p1606744817344800 👍🙂
👍 1
s
@olonho In your example
GlobalScope.launch(Dispatchers.Main)
, wouldn't this launch coroutines for every frame if the effect is false? Sorry if my understanding is wrong, still learning the compose.
🙏 1
o
you’re correct, updated with
remember
👍 1