Who has not played those snake games on old keypad phones ? I wandered if any one has not played because that was only entertainment option was available back then. If you are still interested in that snake game in era of Instagram, Facebook and you tube then lets build a snake game in our android mobile.
We will be building a basic snake game with simple features like
- It will eat food and gets bigger
- if eats itself then gets smaller
- Left, Right, Up and down movement with buttons
Lets discuss some coding logic that we need to build this game –
- Snake – It will a list of boxes which will change their position with respect to time
- Reward – It is a single box which will show up at any random location in game board
- Canvas or Game Board – It is grid of positions which will work as map for the snake and reward
First of all we need two classes which will maintain the directions and snake positions, lets define them –
- Directions – This is enum class which will store 4 directions in it
- Snake Part – It is data class which will hold the smallest part of snake that is the position of one box
enum class Direction { UP, DOWN, LEFT, RIGHT }
data class SnakePart(val x: Int, val y: Int)
Now define some variables for the game screen-
- gridSize – the size of grid of boxes of the game board ( 20 means 20 * 20 rows & columns)
- boxSize – size of one box in the grid ( in dp- density independent pixels)
- snake – a mutable ( changeable) list of snakePart which will contains series of boxes that will make our snake
- direction – any particular direction where is snake is currently going on board
- reward – a box at any random location on the game board
- justAteReward – a Boolean variable that help to increase the size of snake at any moment the snake ate the reward
// Canvas size (e.g., 20x20 grid)
val gridSize = 20
val boxSize = 20.dp
// State for snake, reward, direction, and game over
var snake by remember {
mutableStateOf(
listOf(
SnakePart(10, 10),
SnakePart(10, 11),
SnakePart(10, 12),
SnakePart(10, 13)
)
)
}
var direction by remember { mutableStateOf(Direction.RIGHT) }
var reward by remember {
mutableStateOf(
SnakePart(
Random.nextInt(0, gridSize),
Random.nextInt(0, gridSize)
)
)
}
// State to control whether the snake just ate the reward
var justAteReward by remember { mutableStateOf(false) }
Now its time to give the movement to our snake by changing the position of boxes at every second or milliseconds. So, we will run a infinite loop which will iterate itself after every 200 milliseconds. In each iteration we will update the position of each box with smooth transition in its coordinates that seen as snake movement. For that logical and synchronized movement of boxes one after the other we need a function called moveSnake which takes following arguments-
- snake – the current snake which is actually a list of snakePart
- direction – the present direction in which the snake moving
- grow – a Boolean variable which helps to determine that snake length should be increases or remains the same
This function perform two basic task one is to move the snake and second is to move the snake in right direction. It will take the first snakePart from the snake list and update its coordinate as per the direction and make a new box then based on the grow Boolean it will either add one more box in front or it will add the rest of snakeParts except the last one.
// Function to move the snake
fun moveSnake(snake: List<SnakePart>, direction: Direction, grow: Boolean): List<SnakePart> {
val head = snake.first()
val newHead = when (direction) {
Direction.UP -> SnakePart(head.x, head.y - 1)
Direction.DOWN -> SnakePart(head.x, head.y + 1)
Direction.LEFT -> SnakePart(head.x - 1, head.y)
Direction.RIGHT -> SnakePart(head.x + 1, head.y)
}
return if (grow) {
listOf(newHead) + snake // Add new head and keep the entire body (grow)
} else {
listOf(newHead) + snake.dropLast(1) // Move by adding new head and dropping last part
}
}
after this we need to ensure one more thing that if the snake moved out of our game board then we need a separate logic to show him up on the other side of the board as we usually see. We need a function called wrapped which will take the individual snakePart and gridSize as input and provide us the new snake part which will show up on the other side means wrap the head within the grid only.
// Function to wrap a single position within the grid boundaries
fun wrapPosition(part: SnakePart, gridSize: Int): SnakePart {
return SnakePart(
x = (part.x + gridSize) % gridSize, // if snake @ (20, 10), so (20 + 20) % 20 = 40 % 20 = 0 -->snake reappears at the leftmost position of the grid (0, 10)
y = (part.y + gridSize) % gridSize // if snake @ (5, -1), so (-1 + 20) % 20 = 19 % 20 = 19 -->snake reappears at the bottom of the grid (5, 19)
)
}
Before coming to our loop, let’s discuss our last function which is a simple reward generator. This function will generate a reward at random location each time the snake eats the previous one but ensures that new reward will never comes on the snake itself.
// Function to generate a new reward that doesn't overlap with the snake
fun generateNewReward(snake: List<SnakePart>, gridSize: Int): SnakePart {
var newReward: SnakePart
do {
newReward = SnakePart(Random.nextInt(0, gridSize), Random.nextInt(0, gridSize))
} while (snake.contains(newReward))
return newReward
}
finally come back to our loop and here we will do following tasks –
- move Snake – first in the loop move snake by calling our moveSnake function
- wrap head – take the first head of snake and check & wrap that in game box only if needed
- eat reward – if the head reached to reward then make justAteReward Boolean true and make new reward
- Self eating – if the snake move its head within itself then start reducing the boxes from there until all finishes
- wrap snake – finally wrap the whole snake itself within the boundary of game box
LaunchedEffect(Unit) {
while (true) {
delay(200L) //decide speed, less for more speed
snake = moveSnake(snake, direction, justAteReward)
justAteReward = false // Reset the flag after moving the snake
// Check for collision with reward (including wrapping behavior)
val head = snake.first()
val wrappedHead = wrapPosition(head, gridSize) // Ensure head is wrapped before checking
if (wrappedHead == reward) {
justAteReward = true // Grow the snake
reward = generateNewReward(snake, gridSize) //generate new reward
}
// Check if snake has collided with itself
if (snake.drop(1).contains(wrappedHead)) {
// Reduce snake size from the point of collision
snake = snake.dropWhile { it != wrappedHead }.drop(1)
}
// Wrap the snake within the grid boundaries
snake = snake.map { wrapPosition(it, gridSize) }
}
}
Now its time to draw something on the canvas. We need a canvas composable where we will show our snake and rewards. For the snake movement we need 4 buttons for four different directions. We will drawRect funcition of canvas to draw each part of snake and the reward.
// UI for game
Column(
modifier = Modifier.fillMaxSize().padding(5.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Snake game canvas
Canvas(modifier = Modifier.size(gridSize * boxSize).background(Color.LightGray.copy(0.3f))) {
// Draw snake
snake.forEach { part ->
drawRoundRect(
Color.Green,
topLeft = androidx.compose.ui.geometry.Offset(
part.x * boxSize.toPx(),
part.y * boxSize.toPx()
),
size = androidx.compose.ui.geometry.Size(boxSize.toPx(), boxSize.toPx()),
cornerRadius = CornerRadius(10f, 10f)
)
}
// Draw reward
drawRoundRect(
Color.Red,
topLeft = androidx.compose.ui.geometry.Offset(
reward.x * boxSize.toPx(),
reward.y * boxSize.toPx()
),
size = androidx.compose.ui.geometry.Size(boxSize.toPx(), boxSize.toPx()),
cornerRadius = CornerRadius(10f, 10f)
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { if (direction != Direction.DOWN) direction = Direction.UP },
modifier = Modifier.size(80.dp)
) {
Icon(
imageVector = Icons.Default.KeyboardArrowUp,
contentDescription = ""
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
onClick = { if (direction != Direction.RIGHT) direction = Direction.LEFT },
modifier = Modifier.size(80.dp)
) {
Icon(
imageVector = Icons.Default.KeyboardArrowLeft,
contentDescription = ""
)
}
Button(
onClick = { if (direction != Direction.LEFT) direction = Direction.RIGHT },
modifier = Modifier.size(80.dp)
) {
Icon(
imageVector = Icons.Default.KeyboardArrowRight,
contentDescription = ""
)
}
}
Button(
onClick = { if (direction != Direction.UP) direction = Direction.DOWN },
modifier = Modifier.size(80.dp)
) {
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = ""
)
}
}
}
Each button will simply update the direction variable by first checking the current direcition.
This is all that you need to know to deep dive into the game but if you have not much time then this is the GitHub gist like here