Coding Bihar

Pong Game in Jetpack Compose

Pong Game in Jetpack Compose - Coding Bihar
Pong Game in Jetpack Compose

Pong is a basic two-dimensional sports game designed to mimic the experience of table tennis. The game features two players (or one player against the computer) who each control a paddle placed on opposite sides of the screen. The goal is to use your paddle to hit a bouncing ball, sending it back and forth between players.

The ball can bounce off the paddles and walls, and points are scored when one player fails to return the ball, allowing it to pass their paddle.

Pong is recognized as one of the first arcade video games and was developed by Atari, making its debut in 1972. Its name is derived from the game "ping-pong" (another term for table tennis), which it emulates in a digital form.

Why is it Called "Pong"?

The creators chose the name "Pong" as a shortened version of "ping-pong" because it perfectly captures the back-and-forth action of the game. The name is concise, catchy, and directly ties to the sport the game simulates.
Output Pong Game in Jetpack ComposeAndroid Studio Ladybug

Gameplay:

In Pong, each player controls a vertical paddle that moves up and down along the edge of the screen to hit the ball. The ball continuously bounces between the paddles and the walls. The objective is to make the ball pass your opponent’s paddle, earning a point if they fail to return it. The game is quick, easy to understand, and has a scoring system similar to real-life table tennis.
Overview:
The code implements a simple Pong game using Jetpack Compose for Android. It uses various Compose components like Canvas, Box, and LaunchedEffect to handle game graphics and the main game loop. The game consists of two paddles (one controlled by the player and the other controlled by an AI) and a bouncing ball. The player tries to prevent the ball from passing their paddle, while the AI does the same. The game keeps track of the score and adjusts the ball's velocity after collisions.

Key Components:

1. Game Setup (Paddle, Ball, and Screen Dimensions)

Screen dimensions: The game dynamically adjusts to the screen size of the device by getting the device's width and height in dp (density-independent pixels) and converting them to pixels using LocalDensity.
Ball: The ball's position is represented by a mutable state (ballPosition) and its velocity by another state (ballVelocity).
Paddles: Each paddle has a height of 200 pixels and is represented by mutable states for their vertical position (leftPaddleY and rightPaddleY).

2. LaunchedEffect and Game Loop

The game loop is handled inside a LaunchedEffect(Unit) block, which continuously updates the ball’s position based on its velocity. The loop runs indefinitely and updates every 16 milliseconds to simulate ~60 frames per second (FPS).
LaunchedEffect(Unit) {
    while (true) {
        ballPosition.value = ballPosition.value.copy(
            x = ballPosition.value.x + ballVelocity.value.x,
            y = ballPosition.value.y + ballVelocity.value.y
        )
        // other game logic...
        delay(16L)  // Pause for 16 ms (simulate 60 FPS)
    }
}

3. Ball Movement and Collisions

Ball Movement: The ball’s position is updated in every frame by adding its velocity (ballVelocity) to its current position (ballPosition).
Wall Collisions: The ball bounces off the top and bottom walls. If the ball's y coordinate reaches either the top or the bottom of the screen, the velocity's y component is inverted to make the ball bounce in the opposite direction.
Paddle Collisions: The ball checks for collision with the paddles. When the ball reaches the left or right side of the screen (where the paddles are), it checks if it’s within the paddle’s range. If so, the ball bounces back by inverting the x component of its velocity.
if (ballPosition.value.x - ballRadius <= paddleWidth &&
    ballPosition.value.y in leftPaddleY.floatValue..(leftPaddleY.floatValue + paddleHeight)
) {
    ballVelocity.value = ballVelocity.value.copy(x = -ballVelocity.value.x)
}

4. Score Keeping

If the ball passes beyond the paddles (left or right side of the screen), a point is scored for the opposite player, and the ball is reset to the center of the screen.
if (ballPosition.value.x <= 0) { // Left player misses
    rightPlayerScore.value += 1
    resetBall(ballPosition, ballVelocity, screenWidth, screenHeight)
}

if (ballPosition.value.x >= screenWidth) { // Right player misses
    leftPlayerScore.value += 1
    resetBall(ballPosition, ballVelocity, screenWidth, screenHeight)
}

5. Player Paddle Control

The left paddle is controlled by the player, who can drag the paddle up and down using touch gestures. This is achieved by detectVerticalDragGestures in pointerInput.
.pointerInput(Unit) {
    detectVerticalDragGestures { _, dragAmount ->
        leftPaddleY.floatValue =
            (leftPaddleY.floatValue + dragAmount).coerceIn(0f, screenHeight - paddleHeight)
    }
}

6. AI Paddle Movement

The right paddle is controlled by a simple AI that tries to follow the ball’s y position. It moves up or down, trying to align with the ball.
rightPaddleY.floatValue = ballPosition.value.y.coerceIn(0f, screenHeight - paddleHeight)

7. Drawing the Game

The game is rendered inside a Canvas composable, which allows drawing shapes like rectangles and circles.
Left and Right Paddles are drawn as Rect shapes, while the ball is drawn as a Circle.
Canvas(modifier = Modifier.fillMaxSize()) {
    // Draw left paddle
    drawRect(
        color = Color.Blue,
        topLeft = Offset(0f, leftPaddleY.floatValue),
        size = Size(paddleWidth, paddleHeight)
    )

    // Draw right paddle
    drawRect(
        color = Color.Red,
        topLeft = Offset(size.width - paddleWidth, rightPaddleY.floatValue),
        size = Size(paddleWidth, paddleHeight)
    )

    // Draw the ball
    drawCircle(
        color = Color.Green,
        center = ballPosition.value,
        radius = ballRadius
    )
}

8. Resetting the Ball

When a player scores, the ball is reset to the center of the screen with an inverted velocity to start moving in the opposite direction.
fun resetBall(
    ballPosition: MutableState,
    ballVelocity: MutableState,
    screenWidth: Float,
    screenHeight: Float
) {
    ballPosition.value = Offset(screenWidth / 2, screenHeight / 2)
    ballVelocity.value = ballVelocity.value.copy(
        x = if (ballVelocity.value.x > 0) -6f else 6f,
        y = ballVelocity.value.y
    )
}
MainActivity
Copy this code →

package com.codingbihar.composepractice

import ...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            ComposePracticeTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}
PongGameApp
Copy this code →

package com.codingbihar.composepractice

import ...

@Composable
fun PongGame() {
    // Get screen dimensions based on the device
    val configuration = LocalConfiguration.current
    val screenWidthDp = configuration.screenWidthDp.dp
    val screenHeightDp = configuration.screenHeightDp.dp

    // Use LocalDensity to convert dp to pixels
    val density = LocalDensity.current
    val screenWidth = with(density) { screenWidthDp.toPx() }
    val screenHeight = with(density) { screenHeightDp.toPx() }

    // Game state variables
    val ballPosition = remember { mutableStateOf(Offset(screenWidth / 2, screenHeight / 2)) }
    val ballVelocity = remember { mutableStateOf(Offset(6f, 6f)) }
    val leftPaddleY = remember { mutableFloatStateOf(screenHeight / 2 - 100f) }
    val rightPaddleY = remember { mutableFloatStateOf(screenHeight / 2 - 100f) }
    val leftPlayerScore = remember { mutableIntStateOf(0) }
    val rightPlayerScore = remember { mutableIntStateOf(0) }
    val gameOver = remember { mutableStateOf(false) }
    val winner = remember { mutableStateOf("") }
    val paddleHeight = 200f
    val paddleWidth = 20f
    val ballRadius = 45f
    val winningScore = 5 // Set the winning score

    // Game loop: update ball position
    LaunchedEffect(Unit) {
        while (!gameOver.value) {
            ballPosition.value = ballPosition.value.copy(
                x = ballPosition.value.x + ballVelocity.value.x,
                y = ballPosition.value.y + ballVelocity.value.y
            )

            // Ball bounce off top and bottom walls
            if (ballPosition.value.y <= 0 || ballPosition.value.y >= screenHeight - ballRadius) {
                ballVelocity.value = ballVelocity.value.copy(y = -ballVelocity.value.y)
            }

            // Ball collision with left paddle
            if (ballPosition.value.x - ballRadius <= paddleWidth &&
                ballPosition.value.y in leftPaddleY.floatValue..(leftPaddleY.floatValue + paddleHeight)
            ) {
                ballVelocity.value = ballVelocity.value.copy(x = -ballVelocity.value.x)
            }

            // Ball collision with right paddle
            if (ballPosition.value.x + ballRadius >= screenWidth - paddleWidth &&
                ballPosition.value.x <= screenWidth &&
                ballPosition.value.y in rightPaddleY.floatValue..(rightPaddleY.floatValue + paddleHeight)
            ) {
                ballVelocity.value = ballVelocity.value.copy(x = -ballVelocity.value.x)
            }

            // Ball passes left paddle (right player scores)
            if (ballPosition.value.x <= 0) {
                rightPlayerScore.value += 1
                if (rightPlayerScore.intValue >= winningScore) {
                    gameOver.value = true
                    winner.value = "Player 2 Wins!"
                } else {
                    resetBall(ballPosition, ballVelocity, screenWidth, screenHeight)
                }
            }

            // Ball passes right paddle (left player scores)
            if (ballPosition.value.x >= screenWidth) {
                leftPlayerScore.value += 1
                if (leftPlayerScore.intValue >= winningScore) {
                    gameOver.value = true
                    winner.value = "Player 1 Wins!"
                } else {
                    resetBall(ballPosition, ballVelocity, screenWidth, screenHeight)
                }
            }

            delay(16L) // ~60 FPS
        }
    }

    // Drawing the game
    Box(modifier = Modifier.fillMaxSize()) {
        if (gameOver.value) {
            // Display the winner message when game is over
            Text(
                text = winner.value,
                fontSize = 32.sp,
                fontWeight = FontWeight.Bold,
                color = Color.Green,
                modifier = Modifier.align(Alignment.Center)
            )
        } else {
            // Display the score at the top of the screen
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text(
                    text = "Player 1: ${leftPlayerScore.intValue}",
                    fontSize = 24.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color.Blue
                )
                Text(
                    text = "Player 2: ${rightPlayerScore.intValue}",
                    fontSize = 24.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color.Red
                )
            }

            // Game canvas
            Canvas(modifier = Modifier
                .fillMaxSize()
                .pointerInput(Unit) {
                    detectVerticalDragGestures { _, dragAmount ->
                        // Move left paddle with drag
                        leftPaddleY.floatValue =
                            (leftPaddleY.floatValue + dragAmount).coerceIn(0f, screenHeight - paddleHeight)
                    }
                }
            ) {
                // Draw left paddle
                drawRect(
                    color = Color.Blue,
                    topLeft = Offset(0f, leftPaddleY.floatValue),
                    size = Size(paddleWidth, paddleHeight)
                )

                // Draw right paddle (AI control)
                drawRect(
                    color = Color.Red,
                    topLeft = Offset(size.width - paddleWidth, rightPaddleY.floatValue),
                    size = Size(paddleWidth, paddleHeight)
                )

                // Simple AI: Move the right paddle towards the ball
                rightPaddleY.floatValue = ballPosition.value.y.coerceIn(0f, screenHeight - paddleHeight)

                // Draw the ball
                drawCircle(
                    color = Color.Green,
                    center = ballPosition.value,
                    radius = ballRadius
                )
            }
        }
    }
}

// Reset the ball to the center and invert direction
fun resetBall(
    ballPosition: MutableState,
    ballVelocity: MutableState,
    screenWidth: Float,
    screenHeight: Float
) {
    ballPosition.value = Offset(screenWidth / 2, screenHeight / 2)
    ballVelocity.value = ballVelocity.value.copy(
        x = if (ballVelocity.value.x > 0) -6f else 6f,
        y = ballVelocity.value.y
    )
}

Summary:

Paddles: The player controls the left paddle using vertical drag gestures, and the right paddle is controlled by AI.
Ball: Moves automatically and bounces off the walls and paddles. The game keeps running at ~60 FPS using LaunchedEffect.
Scoring: When the ball passes a paddle, the opposite player scores, and the ball resets to the center.
Rendering: The game elements (paddles and ball) are drawn on the screen using Jetpack Compose’s Canvas.

 Sandeep Gupta

Posted by Sandeep Gupta

Please share your feedback us at:sandeep@codingbihar.com. Thank you for being a part of our community!

Special Message

Welcome to coding bihar!