Snake Game App in Jetpack Compose

Snake Game  App in Jetpack Compose
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)

Snake Game App
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
Previous Post Next Post

Contact Form