Best Tutorial for AndroidDevelopers

Android App Development

Stay ahead with the latest tools, trends, and best practices in Android development

How I Built My First Android Game in Jetpack Compose

How I Built My First Android Game in Jetpack Compose - Responsive Blogger Template
How I Built My First Android Game in Jetpack Compose Thumbnail

How I Built My First Android Game in Jetpack Compose: Space War Adventure 

When I first decided to build my own Android game, I wanted something fun, visually appealing, and completely made with Jetpack Compose. After brainstorming, I landed on a Space War theme—vibrant, fast-paced, and perfect for experimenting with animations, user interactions, and game logic. Here’s my journey building my first Android game.

Why Jetpack Compose for Game Development?

You might think Jetpack Compose is just for building apps with buttons, lists, and forms—but I wanted to challenge myself. Compose offers:
  • Declarative UI: Easy to animate and update components dynamically.
  • Canvas & Graphics: I could draw spaceships, bullets, and explosions directly.
  • State Management: Managing score, lives, and game levels became intuitive.
Using Compose, I didn’t have to rely on traditional XML layouts or third-party engines. It felt like coding the game entirely in Kotlin, which was exciting.

Step 1: Planning the Game

  1. Before touching code, I sketched my ideas:
  2. Player Ship: Controlled by dragging or tapping on the screen.
  3. Enemy Ships: Appear from the top and move down.
  4. Bullets/Lasers: Fired automatically from the player ship.
  5. Power-ups & Explosions: To make the game engaging.
  6. Score & Lives: Track progress and add challenge.
I decided to keep the game simple at first, then enhance it later with boss fights, confetti effects, and sound.

Step 2: Designing Assets

Even for a beginner game, visuals matter. I created:
  1. Spaceships – Player, enemy, and boss ships with vibrant colors.
  2. Laser Bullets – HD, parallel beams for a cinematic feel.
  3. Explosions – Multiple stages of explosions for action feedback.
  4. Background – Starry space background with subtle animations.
All assets were made with transparent backgrounds so I could animate them freely on the Compose Canvas.

Lets Build: Spaceship,  Bullets, Explosan and Background

1. Player and Enemy:

fun DrawScope.drawPlayerShip(player: Offset) {
    // ✨ Glow Effect
    drawCircle(
        color = Color.Cyan.copy(alpha = 0.3f),
        radius = 55f,
        center = player
    )

    // πŸš€ Main Body (Rocket)
    drawRoundRect(
        brush = Brush.verticalGradient(
            colors = listOf(Color(0xFF00E5FF), Color(0xFF2979FF))
        ),
        topLeft = Offset(player.x- 25f, player.y - 40f),
        size = Size(50f, 80f),
        cornerRadius = CornerRadius(25f, 25f)
    )

    // πŸ”Ί Top Triangle (Rocket Nose)
    val path = Path().apply {
        moveTo(player.x, player.y - 70f)
        lineTo(player.x - 25f, player.y - 40f)
        lineTo(player.x + 25f, player.y - 40f)
        close()
    }
    drawPath(path, Color.Red)

    // πŸ‘€ Window
    drawCircle(
        color = Color.White,
        radius = 12f,
        center = Offset(player.x, player.y - 15f)
    )
    drawCircle(
        color = Color.Blue,
        radius = 7f,
        center = Offset(player.x, player.y - 15f)
    )

    // πŸ”₯ Bottom Fire
    drawCircle(
        color = Color.Yellow,
        radius = 12f,
        center = Offset(player.x, player.y + 45f)
    )
    drawCircle(
        color = Color.Red,
        radius = 7f,
        center = Offset(player.x, player.y + 50f)
    )

    // πŸ”» Side Wings
    drawCircle(
        color = Color.Red,
        radius = 10f,
        center = Offset(player.x - 30f, player.y + 10f)
    )
    drawCircle(
        color = Color.Red,
        radius = 10f,
        center = Offset(player.x + 30f, player.y + 10f)
    )
}


fun DrawScope.drawEnemy(enemy: Offset) {

    // πŸ”΅ Body (Gradient Circle)
    drawCircle(
        brush = Brush.radialGradient(
            colors = listOf(Color(0xFFFF6F00), Color(0xFFFFC107)),
            center = enemy,
            radius = 40f
        ),
        radius = 40f,
        center = enemy
    )

    // πŸ‘€ Left Eye
    drawCircle(
        color = Color.White,
        radius = 10f,
        center = Offset(enemy.x - 12f, enemy.y - 8f)
    )

    drawCircle(
        color = Color.Black,
        radius = 5f,
        center = Offset(enemy.x - 12f, enemy.y - 8f)
    )

    // πŸ‘€ Right Eye
    drawCircle(
        color = Color.White,
        radius = 10f,
        center = Offset(enemy.x + 12f, enemy.y - 8f)
    )

    drawCircle(
        color = Color.Black,
        radius = 5f,
        center = Offset(enemy.x + 12f, enemy.y - 8f)
    )

    // 😈 Cute Smile
    drawArc(
        color = Color.Black,
        startAngle = 0f,
        sweepAngle = 180f,
        useCenter = false,
        topLeft = Offset(enemy.x - 15f, enemy.y + 5f),
        size = Size(30f, 20f),
        style = Stroke(width = 4f)
    )

    // πŸ”₯ Bottom Flames
    drawCircle(
        color = Color.Red,
        radius = 8f,
        center = Offset(enemy.x - 15f, enemy.y + 40f)
    )

    drawCircle(
        color = Color.Yellow,
        radius = 8f,
        center = Offset(enemy.x + 15f, enemy.y + 40f)
    )
}

Output:

Player Image
Enemy Image


2.Laser Bullets:

fun DrawScope.drawPlayerLaser(center: Offset) {

    val beamHeight = 40f
    val beamWidth = 10f

    val top = center.y - beamHeight

    // ✨ Outer Glow (aligned properly)
    drawRoundRect(
        color = Color.Red.copy(alpha = 0.25f),
        topLeft = Offset(center.x - beamWidth, top - 5f),
        size = Size(beamWidth * 2, beamHeight + 10f),
        cornerRadius = CornerRadius(12f, 12f)
    )

    // 🌈 Main Beam
    drawRoundRect(
        brush = Brush.verticalGradient(
            colors = listOf(Color.Cyan, Color.Red)
        ),
        topLeft = Offset(center.x - beamWidth / 2, top),
        size = Size(beamWidth, beamHeight),
        cornerRadius = CornerRadius(6f, 6f)
    )

    // πŸ’Ž Core
    drawRoundRect(
        color = Color.Black,
        topLeft = Offset(center.x - 2f, top + 5f),
        size = Size(4f, beamHeight - 10f),
        cornerRadius = CornerRadius(4f, 4f)
    )

    // πŸ”₯ Spark Tip
    drawCircle(
        brush = Brush.radialGradient(
            colors = listOf(Color.White, Color.Blue, Color.Transparent),
            center = Offset(center.x, top),
            radius = 12f
        ),
        radius = 12f,
        center = Offset(center.x, top)
    )
}


fun DrawScope.drawEnemyBullet(center: Offset) {

    // ✨ Outer Glow
    drawCircle(
        color = Color.Magenta.copy(alpha = 0.3f),
        radius = 22f,
        center = center
    )

    // 🌈 Main Plasma Body (Gradient)
    drawCircle(
        brush = Brush.radialGradient(
            colors = listOf(Color.Yellow, Color.Magenta),
            center = center,
            radius = 15f
        ),
        radius = 15f,
        center = center
    )

    // ⚡ Bright Core
    drawCircle(
        color = Color.White,
        radius = 6f,
        center = center
    )

    // πŸ”₯ Motion Tail (below bullet)
    drawRoundRect(
        brush = Brush.verticalGradient(
            colors = listOf(Color.Magenta, Color.Transparent)
        ),
        topLeft = Offset(center.x - 4f, center.y + 15f),
        size = Size(8f, 20f),
        cornerRadius = CornerRadius(4f, 4f)
    )
}

Output:

Laser Bullets

3. Explosions:

// πŸ‘½ Face Base
                drawCircle(
                    color = Color(0xFF81C784).copy(alpha = alpha),
                    radius = faceRadius,
                    center = center
                )

                // πŸ‘€ Left Eye
                drawCircle(
                    color = Color.White.copy(alpha = alpha),
                    radius = 8f * scale,
                    center = Offset(center.x - 12f * scale, center.y - 5f * scale)
                )

                drawCircle(
                    color = Color.Black.copy(alpha = alpha),
                    radius = 4f * scale,
                    center = Offset(center.x - 12f * scale, center.y - 5f * scale)
                )

                // πŸ‘€ Right Eye
                drawCircle(
                    color = Color.White.copy(alpha = alpha),
                    radius = 8f * scale,
                    center = Offset(center.x + 12f * scale, center.y - 5f * scale)
                )

                drawCircle(
                    color = Color.Black.copy(alpha = alpha),
                    radius = 4f * scale,
                    center = Offset(center.x + 12f * scale, center.y - 5f * scale)
                )

                // 😱 Sad Open Mouth
                drawArc(
                    color = Color.Black.copy(alpha = alpha),
                    startAngle = 0f,
                    sweepAngle = 180f,
                    useCenter = true,
                    topLeft = Offset(center.x - 15f * scale, center.y + 5f * scale),
                    size = Size(30f * scale, 25f * scale)
                )

                // πŸ’§ Tear Drops
                drawCircle(
                    color = Color.Cyan.copy(alpha = alpha),
                    radius = 5f * scale,
                    center = Offset(center.x - 18f * scale, center.y + 15f * scale)
                )

                drawCircle(
                    color = Color.Cyan.copy(alpha = alpha),
                    radius = 5f * scale,
                    center = Offset(center.x + 18f * scale, center.y + 15f * scale)
                )

Output:

Explosions Image

4. Background:

// Background
            drawRect(Color.Black)

            stars.forEach { star ->
                drawCircle(
                    color = Color.White,
                    radius = star.radius,
                    center = Offset(star.x, star.y)
                )

                star.y += star.speed

                if (star.y > size.height) {
                    star.y = 0f
                    star.x = Random.nextFloat() * size.width
                }
            }

Output:

Background Design

Final Code


@Composable
fun GameScreen(vm: GameViewModel = viewModel()) {

    Box(
        Modifier
            .fillMaxSize()
            .background(Color.Black)
            .onSizeChanged { size ->
                vm.screenWidth = size.width.toFloat()
                vm.screenHeight = size.height.toFloat()
                vm.playerX = vm.screenWidth / 2f
                vm.playerY = vm.screenHeight * 0.9f
            }

            .pointerInput(Unit) {
                detectDragGestures { _, drag ->
                    vm.shoot()
                    vm.movePlayer(drag.x, drag.y)
                }
            }
    ) {

        val stars = remember {
            List(100) {
                Star(
                    x = Random.nextFloat() * vm.screenWidth -100f,
                    y = Random.nextFloat() * vm.screenHeight -100f,
                    radius = Random.nextFloat() * 5f,
                    speed = Random.nextFloat() * 4f + 1f
                )
            }
        }

        Canvas(Modifier.fillMaxSize()) {

            // Background
            drawRect(Color.Black)

            stars.forEach { star ->
                drawCircle(
                    color = Color.White,
                    radius = star.radius,
                    center = Offset(star.x, star.y)
                )

                star.y += star.speed

                if (star.y > size.height) {
                    star.y = 0f
                    star.x = Random.nextFloat() * size.width
                }
            }

            drawPlayerShip(Offset(vm.playerX, vm.playerY))

            vm.enemies.forEach { enemy ->
                drawEnemy(enemy = Offset(enemy.x, enemy.y))

                if (System.currentTimeMillis() < enemy.showHpUntil) {
                    // πŸ‘‡ Draw HP number above enemy
                    drawContext.canvas.nativeCanvas.drawText(
                        enemy.hp.toString(),
                        enemy.x,
                        enemy.y - 45f,   // position above enemy
                        android.graphics.Paint().apply {
                            color = android.graphics.Color.WHITE
                            textSize = 32f
                            textAlign = android.graphics.Paint.Align.CENTER
                            isFakeBoldText = true
                        }
                    )
                }
            }

            vm.playerBullets.forEach {
                drawPlayerLaser(Offset(it.x, it.y))
            }
            
            vm.enemyBullets.forEach {
                drawEnemyBullet(Offset(it.x, it.y))
            }
            
            vm.explosions.forEach { explosion ->

                val elapsed = System.currentTimeMillis() - explosion.time
                val progress = (elapsed / 600f).coerceIn(0f, 1f)

                val scale = 1f + progress
                val alpha = 1f - progress

                val center = Offset(explosion.x, explosion.y)

                val faceRadius = 35f * scale

                // πŸ‘½ Face Base
                drawCircle(
                    color = Color(0xFF81C784).copy(alpha = alpha),
                    radius = faceRadius,
                    center = center
                )

                // πŸ‘€ Left Eye
                drawCircle(
                    color = Color.White.copy(alpha = alpha),
                    radius = 8f * scale,
                    center = Offset(center.x - 12f * scale, center.y - 5f * scale)
                )

                drawCircle(
                    color = Color.Black.copy(alpha = alpha),
                    radius = 4f * scale,
                    center = Offset(center.x - 12f * scale, center.y - 5f * scale)
                )

                // πŸ‘€ Right Eye
                drawCircle(
                    color = Color.White.copy(alpha = alpha),
                    radius = 8f * scale,
                    center = Offset(center.x + 12f * scale, center.y - 5f * scale)
                )

                drawCircle(
                    color = Color.Black.copy(alpha = alpha),
                    radius = 4f * scale,
                    center = Offset(center.x + 12f * scale, center.y - 5f * scale)
                )

                // 😱 Sad Open Mouth
                drawArc(
                    color = Color.Black.copy(alpha = alpha),
                    startAngle = 0f,
                    sweepAngle = 180f,
                    useCenter = true,
                    topLeft = Offset(center.x - 15f * scale, center.y + 5f * scale),
                    size = Size(30f * scale, 25f * scale)
                )

                // πŸ’§ Tear Drops
                drawCircle(
                    color = Color.Cyan.copy(alpha = alpha),
                    radius = 5f * scale,
                    center = Offset(center.x - 18f * scale, center.y + 15f * scale)
                )

                drawCircle(
                    color = Color.Cyan.copy(alpha = alpha),
                    radius = 5f * scale,
                    center = Offset(center.x + 18f * scale, center.y + 15f * scale)
                )
            }
        }

        Column(Modifier.fillMaxSize()) {

            Text(
                "www.codingbihar.com", color = Color.Green,
                style = MaterialTheme.typography.displaySmall,

            )

            Row(
                modifier = Modifier
                    .systemBarsPadding()
                    .fillMaxWidth()
                    .padding(32.dp),
                horizontalArrangement = Arrangement.SpaceBetween

            ) {

                AnimatedScore(vm.score)

                GlowingHP(vm.playerHP)

                Text(
                    text = "πŸš€ ${vm.playerLives}",
                    fontSize = 22.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color.Cyan
                )
            }
        }
       
        if (vm.isGameOver) {
            Column(
                modifier = Modifier.align(Alignment.Center),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {

                Text(
                    text = "GAME OVER",
                    color = Color.Red,
                    fontSize = 32.sp
                )

                Spacer(modifier = Modifier.height(20.dp))

                Button(onClick = { vm.restart() }) {
                    Text("Restart")
                }
            }
        }
    }
}


@Composable
fun AnimatedScore(score: Int) {

    val scale = remember { Animatable(1f) }

    LaunchedEffect(score) {
        scale.animateTo(1.3f)
        scale.animateTo(1f)
    }

    Text(
        text = "⭐ $score",
        fontSize = 22.sp,
        fontWeight = FontWeight.ExtraBold,
        color = Color.Yellow,
        modifier = Modifier.graphicsLayer {
            scaleX = scale.value
            scaleY = scale.value
        }
    )
}


@Composable
fun GlowingHP(hp: Int) {

    val infiniteTransition = rememberInfiniteTransition()
    val alpha by infiniteTransition.animateFloat(
        initialValue = 0.5f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(500),
            repeatMode = RepeatMode.Reverse
        )
    )

    Row(
        modifier = Modifier.alpha(if (hp <= 1) alpha else 1f)
    ) {
        HeartHP(hp)
    }
}

GameViewModel:

class GameViewModel : ViewModel() {

    var screenWidth by mutableFloatStateOf(0f)
    var screenHeight by mutableFloatStateOf(0f)

    var playerX by mutableFloatStateOf(0f)
    var playerY by mutableFloatStateOf(0f)

    var playerLives by mutableIntStateOf(5)
    var playerHP by mutableIntStateOf(5)
        private set

    var score by mutableIntStateOf(0)
    var isGameOver by mutableStateOf(false)

    val enemies = mutableStateListOf()
    val playerBullets = mutableStateListOf()
    val enemyBullets = mutableStateListOf()
    val explosions = mutableStateListOf()

    private val enemyTypes = listOf(
        EnemyType(1, hp = 5, speed = 4f, bulletSpeed = 8f, image = 0),
        EnemyType(2, hp = 10, speed = 2.5f, bulletSpeed = 6f, image = 1),
        EnemyType(3, hp = 15, speed = 2f, bulletSpeed = 6f, image = 2),
        EnemyType(4, hp = 20, speed = 3f, bulletSpeed = 10f, image = 3)
    )

    init {
        startGameLoop()
    }

    private fun startGameLoop() {
        viewModelScope.launch {
            while (isActive) {
                if (!isGameOver && screenWidth > 0f) {
                    updateGame()
                }
                delay(16)
            }
        }
    }
    private fun updateGame() {
        movePlayerBullets()
        moveEnemyBullets()
        moveEnemies()
        spawnEnemies()
        checkCollisions()
        updateExplosions()
    }

    fun movePlayer(dx: Float, dy: Float) {
        if (!isGameOver && screenWidth > 0f) {
            playerX = (playerX + dx).coerceIn(40f, screenWidth - 40f)
            playerY = (playerY + dy).coerceIn(40f, screenHeight - 40f)
        }
    }
    fun shoot() {
        playerBullets.add(
            Bullet(
                x = playerX,
                y = playerY - 80f,
                speed = 80f,
                damage = if ( score > 200) 5 else 1,
                fromEnemy = false,
                type = 0
            )
        )
    }
    private fun spawnEnemies() {
        if ((0..80).random() < 3) {
            val type = enemyTypes.random()
            enemies.add(
                Enemy(
                    x = (50..(screenWidth.toInt() - 50)).random().toFloat(),
                    y = -50f,
                    hp = type.hp,
                    type = type.id
                )
            )
        }
    }

    private fun moveEnemies() {
        enemies.forEach { enemy ->
            val type = enemyTypes.first { it.id == enemy.type }
            enemy.y += type.speed

            if ((0..100).random() < 2) {
                enemyBullets.add(
                    Bullet(
                        x = enemy.x,
                        y = enemy.y,
                        speed = type.bulletSpeed,
                        damage = 1,
                        fromEnemy = true,
                        type = 0
                    )
                )
            }

            if (enemy.y > screenHeight + 50f) {
                enemy.isAlive = false
            }
        }

        enemies.removeAll { !it.isAlive }
    }

    private fun movePlayerBullets() {
        playerBullets.forEach { it.y -= it.speed }
        playerBullets.removeAll { it.y < -50f }
    }

    private fun moveEnemyBullets() {
        enemyBullets.forEach { it.y += it.speed }
        enemyBullets.removeAll { it.y > screenHeight + 50f }
    }

    private fun checkCollisions() {

        for (i in playerBullets.lastIndex downTo 0) {
            val bullet = playerBullets[i]

            for (j in enemies.lastIndex downTo 0) {
                val enemy = enemies[j]

                if (distance(bullet.x, bullet.y, enemy.x, enemy.y) < 40f) {

                    enemy.hp -= bullet.damage
                    enemy.showHpUntil = System.currentTimeMillis() + 600L
                    playerBullets.removeAt(i)

                    if (enemy.hp <= 0) {
                        score += 10
                        explosions.add(
                            Explosion(enemy.x, enemy.y, System.currentTimeMillis())
                        )
                        enemies.removeAt(j)
                        enemyBullets.removeAll(enemyBullets)

                    }
                    break
                }
            }
        }

        for (i in enemyBullets.lastIndex downTo 0) {
            val bullet = enemyBullets[i]

            if (distance(bullet.x, bullet.y, playerX, playerY) < 40f) {
                playerHP--
                enemyBullets.removeAt(i)

                if (playerHP <= 0) {
                    playerLives--

                    if (playerLives > 0) {
                        // Reset HP when life lost
                        playerHP = 5

                        // Optional: Respawn player at center
                        playerX = screenWidth / 2
                        playerY = screenHeight - 150f
                    } else {
                        playerLives = 0
                        playerHP = 0
                        isGameOver = true
                    }

                }
            }
        }
    }

    private fun updateExplosions() {
        val now = System.currentTimeMillis()
        explosions.removeAll { now - it.time > 400 }
    }

    private fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
        return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
    }

    fun restart() {
        enemies.clear()
        playerBullets.clear()
        enemyBullets.clear()
        explosions.clear()
        score = 0
        playerHP = 5
        playerLives = 5
        isGameOver = false
    }
}

DataModel:


data class Explosion(
    var x: Float,
    var y: Float,
    var time: Long
)

data class Enemy(
    var x: Float,
    var y: Float,
    var hp: Int,
    var isAlive: Boolean = true,
    val type: Int,
    var showHpUntil: Long = 0L
)

data class Bullet(
    var x: Float,
    var y: Float,
    val speed: Float,
    val damage: Int,
    val fromEnemy: Boolean,
    val type: Int
)

data class EnemyType(
    val id: Int,
    val hp: Int,
    val speed: Float,
    val bulletSpeed: Float,
    val image: Int
)

data class Star(
    var x: Float,
    var y: Float,
    val radius: Float,
    val speed: Float
)

MainActivity:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            BharatSpaceBattleTheme {

                GameScreen()
                
            }
        }
    }
}

Here is 2D game using Jetpack Compose I built


2D game using Jetpack Compose Screenshot 1

2D game using Jetpack Compose Screenshot 2

Sandeep Kumar - Android Developer

About the Author

Sandeep Kumar is an Android developer and educator who writes beginner-friendly Jetpack Compose tutorials on CodingBihar.com. His focus is on clean UI, Material Design 3, and real-world Android apps.

SkillDedication

— Python High Level Programming Language- Expert-Written Tutorials, Projects, and Tools—

Coding Bihar

Welcome To Coding BiharπŸ‘¨‍🏫