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.
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 ->
PongGame()
}
}
}
}
}
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.