Welcome to pay attention to the public number: Ananzhuo to learn more knowledge
inspiration
I was inspired by fundroid’s article about Composing Tetris for Compose, and decided to bring Nokia’s Snake to Android as well
The interface and ideas for this article refer to the fundroid article
Teleport: fundroid tetris blog.csdn.net/vitaviva/ar…
The final result
Send a result in advance, let the reader have a psychological expectation, logic is really not complicated, be sure to finish reading patiently
Train of thought
Our interface is divided into two parts, the top half is the dynamic display area of the game, and the bottom half is the operation area
Display area
The display area can also be subdivided into two parts,
- Border + black border
- Screen dynamic display area
So we split the two parts into @composable BaseScreen and @Composable GameScreen
BaseScreen is used to draw borders and black lines, this part only needs to be drawn once, so we need to pull it out
GameScreen is used to draw dynamic areas, mainly the base light-colored brick matrix, and the snake dark brick, free snake dark brick
Operating area
The operation area also has two parts,
- The start reset button and the Resume pause button are used to control the state of the game
- Arrow button Used to control the direction of the snake head
The operation area group name is @composable OperateBody.
The action status button is implemented by wrapping two @Composable GameButtons around a Row
The d-pad button is implemented with a Box wrapped around the four @composable DirectionButtom
logic
How to communicate data
Use the stateflow. collectAsState method to get the Compose state and refresh the interface whenever the state changes
How to refresh the game state
An internal loop is defined using The LauncherEffect to maintain the clock, allowing us to update the game state at custom intervals.
State snake-body.
We use an array to maintain all the snake bricks, and then the logic of snake moving, snake eating free snake, snake hitting the wall
The snake mobile
If the snake simply moves, we can do this by creating a new element, the first element of the new array is the block under the snake to move to, then adding addAll to the old snake array, and finally removing the last snake in the new snake array. Example Refresh the StateFlow state
Snakes eat free snakes
If the next tile the snake moves to happens to be the free tile, we insert the free tile directly into the first element of the array and do not remove the last tile of the snake array
The snake hit the wall
The entire screen is rasterized as a Width_Matrix * Height_Matrix array. If the snake’s head is outside the raster array, the snake hits the wall and dies. Game over.
The code analysis
StateFlow declaration and use
Declare StateFlow, which does not need to introduce any dependencies
private val _flow = MutableStateFlow(SnakeState(action = Action.GameTick))
val stateFlow = _flow.asStateFlow()
Copy the code
Use StateFlow to get the state
val model = viewModel<GameViewModel>()
val state = model.stateFlow.collectAsState().value
Copy the code
Here SnakeState is the state entity we defined as follows:
data class SnakeState(
var action: Action,
val direction: Direction = Direction.Bottom,
val snakeBody: MutableList<SnakePart> = mutableListOf(SnakePart(10.10)),
val freeSnakePart: SnakePart = SnakePart(15.15),
var Width_Matrix: Int = 0.var Height_Matrix: Int = 0.var isRunning: Boolean = true
)
Copy the code
Game state clock code
Use the LauncherEffect to maintain an infinite loop and distribute a clock at 300ms
val model = viewModel<GameViewModel>()
LaunchedEffect(key1 = Unit) {
while (isActive) {
delay(300)
model.dispatch()
}
}
Copy the code
Snake movement, eat free snake body, hit the wall code
fun dispatch(a) {
val state = stateFlow.value
if(state.action! =Action.GameTick){return
}
if(! state.isRunning) {// The pause state does not update data
return
}
val firstPart = state.snakeBody.first()
val newList = mutableListOf<SnakePart>()
if (state.direction == Direction.Left || state.direction == Direction.Right) {
val x = firstPart.x + state.direction.increase
if (x == state.freeSnakePart.x && firstPart.y == state.freeSnakePart.y) {// Eat free snake body
newList.add(state.freeSnakePart)
newList.addAll(state.snakeBody)
emit(state.copy(snakeBody = newList, freeSnakePart = newFreeSnakePart()))
return
}
if (x > state.Width_Matrix || x < 0) {/ / the wall
emit(
state.copy(
action = Action.GameOver,
)
)
return
} else {
newList.add(SnakePart(x, firstPart.y))
newList.addAll(state.snakeBody)
newList.removeLast()// Delete the last section of the snake}}else {
val y = firstPart.y + state.direction.increase
if (y == state.freeSnakePart.y && firstPart.x == state.freeSnakePart.x) {// Eat free snake body
newList.add(state.freeSnakePart)
newList.addAll(state.snakeBody)
emit(state.copy(snakeBody = newList, freeSnakePart = newFreeSnakePart()))
return
}
if (y > state.Height_Matrix || y < 0) {/ / the wall
emit(
state.copy(
action = Action.GameOver,
)
)
return
} else {
newList.add(SnakePart(firstPart.x, y))
newList.addAll(state.snakeBody)
newList.removeLast()// Delete the last section of the snake
}
}
emit(state.copy(snakeBody = newList))
}
fun emit(state: SnakeState) {
_flow.value = state
}
Copy the code
Unrealized part
- Snake strikes its own snake logic
- An animation of the game ending after the snake hits the wall
If you are interested in this section, you can fork the code and implement it yourself.
Code: github.com/ananananzhu…