31
loading...
This website collects cookies to deliver better user experience
fun ComposeDash() {
key = remember { mutableStateOf(0L) }
levelData = remember(key.value) {
createLevelData()
}
enemies = remember(key.value) { createEnemies(levelData) }
val gemsTotal = remember(key.value) { Collections.frequency(levelData, CHAR_GEM) }
val gemsCollected = remember(key.value) { mutableStateOf(0) }
// Must be reset explicitly
val lastLives = remember { mutableStateOf(NUMBER_OF_LIVES) }
lives = remember { mutableStateOf(NUMBER_OF_LIVES) }
Box {
LazyVerticalGrid(
lateinit var enemies: SnapshotStateList<Enemy>
lateinit var levelData: SnapshotStateList<Char>
lateinit var lives: MutableState<Int>
lateinit var key: MutableState<Long>
ComposeDash()
is the relevant root composable for the game play, so I remember
all game-relevant states here. Please recall that whenever a remembered state changes, a recomposition will take place. The initial value is usually computed once. Both lastLives
and lives
are set to NUMBER_OF_LIVES
. If I change the value, for example like in the following code snippet, there is no way for ComposeDash()
to know what the initial value has been.private suspend fun freeFall(
levelData: SnapshotStateList<Char>,
current: Int,
what: Char,
lives: MutableState<Int>
) {
lifecycleScope.launch {
delay(800)
if (levelData[current] == what) {
freeFall(levelData, current - COLUMNS, what, lives)
val x = current % COLUMNS
var y = current / COLUMNS + 1
var pos = current
var playerHit = false
while (y < ROWS) {
val newPos = y * COLUMNS + x
when (levelData[newPos]) {
CHAR_BRICK, CHAR_ROCK, CHAR_GEM -> {
break
}
CHAR_PLAYER -> {
if (!playerHit) {
playerHit = true
lives.value -= 1
}
}
}
levelData[pos] = CHAR_BLANK
levelData[newPos] = what
y += 1
pos = newPos
delay(200)
}
}
}
}
lives
to its initial value, I must know what this has been. I'll show you shortly when and where this is done. But first, let's take a closer look to some other states.key = remember { mutableStateOf(0L) }
levelData = remember(key.value) {
createLevelData()
}
levelData
holds the level data and reflects all changes since the initial creation. For example,SnapshotStateList<Char>
to hold the level data. I want any change to cause a recomposition. remember
? There are situations in the game when you want to reset (almost) all states to their initial values. This, for example, is the case if the user has made a bad move, thus the player is hit by a rock or gem. The number of lives is decremented, but the number of collected gems must be set to zero, and the level data must be brought to its initial state, too. While you could do this in a function, there is a much more convenient way: if you pass a key to remember
the value is recalculated when that key changes. Take a look:@Composable
fun NextTry(
key: MutableState<Long>,
lives: MutableState<Int>,
lastLives: MutableState<Int>
) {
val canTryAgain = lives.value > 0
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color(0xa0000000))
.clickable {
if (canTryAgain)
lastLives.value = lives.value
else {
lives.value = NUMBER_OF_LIVES
lastLives.value = NUMBER_OF_LIVES
}
key.value += 1
}
) {
Text(
"I am sorry!\n${if (canTryAgain) "Try again" else "You lost"}",
style = TextStyle(fontSize = 48.sp, textAlign = TextAlign.Center),
color = Color.White,
modifier = Modifier
.align(Alignment.Center)
)
}
}
key.value += 1
leads to all remembered values that are bound to key
will have their values recomputed. Please note that using a true counter here is not necessary, even a Boolean
would have sufficed. I just wanted to show that you can even measure the number of changes, if you need to. In the code snippet above you can also see why lives
and lastLives
have no key. The logic when to set their values to the initial one just differs. Here's another example:@Composable
fun RestartButton(
key: MutableState<Long>, scope: BoxScope,
lives: MutableState<Int>,
lastLives: MutableState<Int>
) {
scope.run {
Text(
POWER.unicodeToString(),
style = TextStyle(fontSize = 32.sp),
color = Color.White,
modifier = Modifier
.align(Alignment.BottomStart)
.clickable {
lives.value = NUMBER_OF_LIVES
lastLives.value = NUMBER_OF_LIVES
key.value += 1
}
)
}
}
val level = """
########################################
#...............................X......#
#.......OO.......OOOOOO................#
#.......OO........OOOOOO...............#
#.......XXXX!.......!X.................#
#......................................#
#.........................##############
#.........OO...........................#
#.........XXX..........................#
##################.....................#
#......................XXXXXX..........#
# OOOOOOO........................#
# !.X......................@......#
########################################
""".trimIndent()
const val SPIDER = 0x1F577
const val CHAR_SPIDER = '!'
!
becomes 🕷enemies
likes this: lateinit var enemies: SnapshotStateList<Enemy>
. Each spider is represented by an instance of a data class which holds its current location (index
) and the direction it is traveling. moveEnemies()
take care of the movement.data class Enemy(var index: Int) {
var dirX = 0
var dirY = 0
}
suspend fun moveEnemies() {
delay(200)
var playerHit = false
if (::enemies.isInitialized) {
val indexPlayer = levelData.indexOf(CHAR_PLAYER)
val colPlayer = indexPlayer % COLUMNS
val rowPlayer = indexPlayer / COLUMNS
enemies.forEach {
if (!playerHit) {
val current = it.index
val row = current / COLUMNS
val col = current % COLUMNS
var newPos = current
if (col != colPlayer) {
if (it.dirX == 0)
it.dirX = if (col >= colPlayer) -1 else 1
newPos += it.dirX
val newCol = newPos % COLUMNS
if (newCol < 0 || newCol >= COLUMNS || levelData[newPos] != CHAR_BLANK) {
if (isPlayer(levelData, newPos)) {
playerHit = true
}
newPos = current
it.dirX = -it.dirX
}
}
if (row != rowPlayer) {
val temp = newPos
if (it.dirY == 0)
it.dirY = if (row >= rowPlayer) -COLUMNS else COLUMNS
newPos += it.dirY
val newRow = newPos / COLUMNS
if (newRow < 0 || newRow >= ROWS || levelData[newPos] != CHAR_BLANK) {
if (isPlayer(levelData, newPos)) {
playerHit = true
}
newPos = temp
it.dirY = 0
}
}
if (newPos != it.index) {
levelData[newPos] = CHAR_SPIDER
levelData[it.index] = CHAR_BLANK
it.index = newPos
}
}
}
}
if (playerHit) {
lives.value -= 1
key.value += 1
}
}
fun isPlayer(levelData: SnapshotStateList<Char>, index: Int) = levelData[index] == CHAR_PLAYER
ComposeDash()
(after levelData
has been filled).enemies = remember(key.value) { createEnemies(levelData) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeDashTheme {
Surface(color = Color.Black) {
ComposeDash()
}
}
}
lifecycleScope.launch {
while (isActive) moveEnemies()
}
}