This code implements a basic Snake game using Jetpack Compose, featuring navigation between screens, real-time game updates, and a simple user interface for gameplay. It handles game logic, rendering, and user input effectively within the Composable functions provided by Jetpack Compose.
Sets up the navigation for the app using NavHost.
What we need?
Screens:
Three screens: Start, Game, and Result is required.
StartScreenSnake: Displays the welcome screen with a logo and a button to start the game.
SnakeGameScreen: Contains the game logic, the snake, and the controls.
ResultScreen: Displays the game over message and score, with options to play again or exit.
1. Navigation Setup
This sets up a navigation graph where StartScreenSnake is the initial screen, SnakeGameScreen is where the game is played, and ResultScreen displays the score after the game ends.
2. Start Screen (StartScreenSnake)
Displays an image, a welcome message, and a button to navigate to the Snake game screen.
3. Game Screen (SnakeGameScreen)
Manages the game state, including the snake's position, food position, score, and game over status.
Uses a LaunchedEffect coroutine to update the snake's position continuously while the game is running.
Calls helper functions to move the snake and check for collisions.
The snake moves every 200 milliseconds, and it checks for collisions to determine if the game is over.
4. Input Handling (SnakeGameInput)
Provides directional buttons (Up, Down, Left, Right) to control the snake.
Contains a pause/play toggle button.
5. Drawing the Game (SnakeGameGrid)
Renders the snake and the food using a Canvas composable.
The snake is drawn with different styles for the head and body.
Each function draws its respective part using the DrawScope methods, such as drawArc and drawCircle.
6. Collision Detection and Game Logic
Functions handle the movement of the snake, growth when eating food, and checking for collisions with walls or itself.
7. Result Screen (ResultScreen)
Displays the final score and allows the player to either restart the game or exit the app.
MainActivity
Copy this code →
package com.example.gamefeeding
import android.media.MediaPlayer
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.gamefeeding.ui.theme.GameFeedingTheme
import kotlinx.coroutines.delay
import kotlin.random.Random
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
GameFeedingTheme {
/* Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
SnakeGameApp()
}
}
}
}
SnakeGameApp
Copy this code →
@Composable
fun SnakeGameApp() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "start") {
composable("start") {
StartScreen(navController)
}
composable("start_game") {
SnakeGameScreen(navController)
}
composable(
"result_screen/{score}",
arguments = listOf(navArgument("score") { type = NavType.IntType })
) { backStackEntry ->
val score = backStackEntry.arguments?.getInt("score") ?: 0
ResultScreen(navController, score)
}
}
}
@Composable
fun ResultScreen(navController: NavController, score: Int) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Black)
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Game Over!",
fontSize = 32.sp,
color = Color.Red,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Your Score: $score",
fontSize = 24.sp,
color = Color. Green,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
Button(onClick = {
// Navigate back to the start screen to play again
navController.navigate("start_game") {
popUpTo("result_screen") { inclusive = true }
}
}) {
Text("Play Again")
}
}
}
@Composable
fun StartScreen(navController: NavController) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(onClick = { navController.navigate("start_game") }
) {
Text("Start Game")
}
}
}
@Composable
fun SnakeGameScreen(navController: NavController) {
val context = LocalContext.current
val mediaPlayer = remember {
// Check if the sound resource is valid
MediaPlayer.create(context, R.raw.eat_sound)
//?: throw IllegalArgumentException("Sound resource not found")
}
var screenWidthPx by remember { mutableIntStateOf(0) }
var screenHeightPx by remember { mutableIntStateOf(0) }
var isPlaying by remember { mutableStateOf(true) } // Start as false to avoid immediate game start
val cellSize = with(LocalDensity.current) { (20.dp).toPx().toInt() }
var snake by remember { mutableStateOf(listOf(Position(10 * cellSize, 10 * cellSize))) }
var direction by remember { mutableStateOf(Direction.RIGHT) }
var score by remember { mutableIntStateOf(0) }
// Food with random position and color
var food by remember {
mutableStateOf(
Food(
position = Position(0, 0), // Temporary position until screen size is set
color = Color.Green
)
)
}
LaunchedEffect(isPlaying, screenWidthPx, screenHeightPx) {
if (screenWidthPx > 0 && screenHeightPx > 0 && isPlaying) { // Check valid dimensions and if playing
food = Food(
position = Position(
x = Random.nextInt(1, (screenWidthPx / cellSize) - 1) * cellSize,
y = Random.nextInt(1, ((screenHeightPx - 100) / cellSize) - 1) * cellSize
),
color = Color(
red = Random.nextInt(256) / 255f,
green = Random.nextInt(256) / 255f,
blue = Random.nextInt(256) / 255f
)
)
while (isPlaying) {
snake = moveSnake(
snake, direction, screenWidthPx, screenHeightPx - 100, cellSize, mediaPlayer, navController, score
)
// Check if snake's head collides with the food
if (snake.first().x == food.position.x && snake.first().y == food.position.y) {
mediaPlayer.start() // Play sound when food is eaten
score += 1
// Grow the snake and move food
snake = snake + snake.last()
food = Food(
position = Position(
Random.nextInt(screenWidthPx / cellSize) * cellSize,
Random.nextInt((screenHeightPx - 100) / cellSize) * cellSize
),
color = Color(
red = Random.nextInt(256) / 255f,
green = Random.nextInt(256) / 255f,
blue = Random.nextInt(256) / 255f
)
)
}
delay(150L)
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.onSizeChanged { size ->
screenWidthPx = size.width
screenHeightPx = size.height
},
verticalArrangement = Arrangement.SpaceBetween
) {
// Display Score at the top
Text(
text = "Score: $score",
color = Color.Black,
fontSize = 24.sp,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
textAlign = TextAlign.End
)
// Game area
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(Color.Black)
) {
Canvas(modifier = Modifier.fillMaxSize()) {
snake.forEach {
drawCircle(
color = Color.Green,
radius = cellSize / 2f,
center = Offset(it.x.toFloat(), it.y.toFloat())
)
}
drawCircle(
color = food.color,
radius = cellSize / 2f,
center = Offset(food.position.x.toFloat(), food.position.y.toFloat())
)
}
}
// Control Buttons
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(onClick = { direction = Direction.LEFT }) { Text("Left") }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { direction = Direction.UP }) { Text("Up") }
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { direction = Direction.DOWN }) { Text("Down") }
}
Button(onClick = { direction = Direction.RIGHT }) { Text("Right") }
}
// Play/Pause Button and Score Display
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceAround
) {
Button(onClick = { isPlaying = !isPlaying }) {
Text(if (isPlaying) "Pause" else "Play")
}
Text("Score: $score", color = Color.White)
}
}
}
fun moveSnake(
snake: List,
direction: Direction,
maxWidth: Int,
maxHeight: Int,
cellSize: Int,
mediaPlayer: MediaPlayer,
navController: NavController,
score: Int // Added score parameter
): List {
val head = snake.first()
val newHead = when (direction) {
Direction.UP -> head.copy(y = head.y - cellSize)
Direction.DOWN -> head.copy(y = head.y + cellSize)
Direction.LEFT -> head.copy(x = head.x - cellSize)
Direction.RIGHT -> head.copy(x = head.x + cellSize)
}
// Play sound if snake touches the edges
if (newHead.x < 0 || newHead.x >= maxWidth || newHead.y < 0 || newHead.y >= maxHeight) {
if (!mediaPlayer.isPlaying) {
mediaPlayer.start()
navController.navigate("result_screen/${score}"){
popUpTo("start_game"){
inclusive = true
}
}
}
}
// Ensure the snake stays within the screen bounds
val boundedHead = Position(
x = newHead.x.coerceIn(0, maxWidth - cellSize),
y = newHead.y.coerceIn(0, maxHeight - cellSize)
)
return listOf(boundedHead) + snake.dropLast(1)
}
GameState
Copy this code →
package com.example.gamefeeding
import androidx.compose.ui.graphics.Color
// Data Classes
enum class Direction { UP, DOWN, LEFT, RIGHT }
data class Position(val x: Int, val y: Int)
data class Food(val position: Position, val color: Color)
Copy this code →
package com.example.gamefeeding
import androidx.compose.ui.graphics.Color
// Data Classes
enum class Direction { UP, DOWN, LEFT, RIGHT }
data class Position(val x: Int, val y: Int)
data class Food(val position: Position, val color: Color)
01
Snake Game App
Full source code with app icon, atrractive UI. It handles game logic, rendering, and user input effectively within the Composable functions provided by Jetpack Compose.
₹199.00 Source Code