31
loading...
This website collects cookies to deliver better user experience
x
and y
we get the index through y * 40 + x
. The other way round is equally easy: x = index % 40
and y = index / 40
. x
, y
, or both change. To move upward (north) y
is decremented by 1. To go south, we increment y
by 1. The same applies to x
regarding west (-1) and east (+1). If we want to implement movement using the index we add or subtract 40 to go north or south. To go west or east we subtract or add 1. Let's see how I make use of this in Compose Dash. Please recall that currently each cell is represented through a Text()
.Text(
modifier = Modifier
.background(background)
.clickable {
movePlayerTo(levelData, index, gemsCollected)
},
text = symbol.unicodeToString()
)
movePlayer()
is invoked. It receives the index
of the location to move to. Please recall that I have defined the game screen like this:private fun createLevelData(): SnapshotStateList<Char> {
val data = mutableStateListOf<Char>()
var rows = 0
level.split("\n").forEach {
if (it.length != COLUMNS)
throw RuntimeException("length of row $rows is not $COLUMNS")
data.addAll(it.toList())
rows += 1
}
if (rows != ROWS)
throw RuntimeException("number of rows is not $ROWS")
return data
}
SnapshotStateList<Char>
. This list is used inside LazyVerticalGrid()
and passed to itemsIndexed()
. Now let's take a look at movePlayer()
. Please remember, it is passed an index
which represents the new location.private fun movePlayerTo(
levelData: SnapshotStateList<Char>,
desti: Int,
gemsCollected: MutableState<Int>
) {
val start = levelData.indexOf(CHAR_PLAYER)
if (start == desti) return
val startX = start % COLUMNS
val startY = start / COLUMNS
val destiX = desti % COLUMNS
val destiY = desti / COLUMNS
val dirX = if (destiX > startX) 1 else -1
val dirY = if (destiY > startY) 1 else -1
var current = start
lifecycleScope.launch {
var x = startX
var y = startY
while (current != -1 && y != destiY) {
current = walk(levelData, current, x, y, gemsCollected)
y += dirY
}
while (current != -1 && current != desti) {
current = walk(levelData, current, x, y, gemsCollected)
x += dirX
}
}
}
val start = levelData.indexOf(CHAR_PLAYER)
(a rather simplistic approach, by the way). As there is always exactly one player on the game screen I can search for it in the levelData
list. Another (and possibly faster) way would be to just store it in some variable. Now, let's look at movement.if (start == desti) return
means: if there has been no movement we don't have anything to do.val dirX = if (destiX > startX) 1 else -1
val dirY = if (destiY > startY) 1 else -1
index
we move through simple arithmetics. The two lines above make this particularly convenient as we need not bother if we want to add or subtract. We can always add dirX
or dirY
because if we go upward or left, the value is -1. Adding -1 is the same as subtracting 1.current
) is -1.var x = startX
var y = startY
while (current != -1 && y != destiY) {
current = walk(levelData, current, x, y, gemsCollected)
y += dirY
}
while (current != -1 && current != desti) {
current = walk(levelData, current, x, y, gemsCollected)
x += dirX
}
movePlayerTo()
orchestrates the movement of the player. Inside a lifecycleScope.launch {
a function named walk()
is invoked repeatedly.movePlayerTo()
has been called from a composable it must return as soon as it can. That's why the actual movement must take place asynchronously. And, as you shall see shortly, moving around may well trigger other concurrent tasks. But before that, let's see what walk()
does.private suspend fun walk(
levelData: SnapshotStateList<Char>,
current: Int,
x: Int,
y: Int,
gemsCollected: MutableState<Int>
): Int {
val newPos = (y * COLUMNS) + x
when (levelData[newPos]) {
CHAR_GEM -> {
gemsCollected.value += 1
}
CHAR_ROCK, CHAR_BRICK -> {
return -1
}
}
levelData[current] = ' '
levelData[newPos] = CHAR_PLAYER
delay(200)
if (current != -1) {
freeFall(levelData, current - COLUMNS, CHAR_ROCK)
freeFall(levelData, current - COLUMNS, CHAR_GEM)
}
return newPos
}
levelData
at the current
position and act accordingly. For example, detecting a collision with a rock or brick is as simple asCHAR_ROCK, CHAR_BRICK -> {
return -1
}
delay(200)
makes sure that the function does not return too early.freeFall()
. It let's rocks or gems fall down. Have you noticed that I pass current - COLUMNS
? This means check what's above the player. So we trigger the movement of objects that are directly above the current location.private suspend fun freeFall(
levelData: SnapshotStateList<Char>,
current: Int,
what: Char
) {
if (levelData[current] == what) {
lifecycleScope.launch {
delay(200)
freeFall(levelData, current - COLUMNS, what)
val x = current % COLUMNS
var y = current / COLUMNS + 1
var pos = current
while (y < ROWS) {
val newPos = y * COLUMNS + x
when (levelData[newPos]) {
CHAR_BRICK, CHAR_ROCK, CHAR_GEM -> {
break
}
}
levelData[pos] = ' '
levelData[newPos] = what
y += 1
pos = newPos
delay(200)
}
}
}
}
lifecycleScope.launch {
. After a short delay freeFall()
is invoked with a location above the current object. This recursion is necessary to have everything above the player fall down eventually. Besides that, the movement of the current object takes place. Here, too, we have some collision checks to determine the fate of the player, rock, or gem.