Pong
Pong is your first complete game. You play against the computer, and the first side to reach 5 points wins. The program uses the ideas from the lessons so far, with one new idea: the game state.
Click the game, then press Space to serve. Use W and S, or the Up and Down arrows, to move your paddle. This game needs a physical keyboard.
The small squares at the top are the score. If you win, the background turns green. If you lose, it turns red. Press Space to play again.
The full program
This program is longer than a lesson, so read it one part at a time.
/* Pong: you against the computer. The first side to reach 5 points wins. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdlib.h>
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define MAX_DT 0.05f /* seconds */
#define PADDLE_WIDTH 12.0f
#define PADDLE_HEIGHT 72.0f
#define PADDLE_MARGIN 24.0f
#define BALL_SIZE 12.0f
#define PLAYER_SPEED 340.0f /* pixels per second */
#define COMPUTER_SPEED 250.0f
#define BALL_START_SPEED 280.0f
#define BALL_SPEED_UP 20.0f
#define BALL_MAX_SPEED 450.0f
#define BALL_STEEPNESS 1.0f /* The largest vertical speed, as a part of the ball speed. */
#define WINNING_SCORE 5
typedef enum {
STATE_SERVE, /* The ball waits for the Space key. */
STATE_PLAY,
STATE_GAME_OVER
} GameState;
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static Uint64 last_ticks = 0;
static GameState state = STATE_SERVE;
static SDL_FRect player;
static SDL_FRect computer;
static SDL_FRect ball;
static float ball_vx = 0.0f; /* pixels per second */
static float ball_vy = 0.0f;
static float ball_speed = 0.0f;
static int player_score = 0;
static int computer_score = 0;
static bool overlap(const SDL_FRect *a, const SDL_FRect *b)
{
return a->x < b->x + b->w && a->x + a->w > b->x &&
a->y < b->y + b->h && a->y + a->h > b->y;
}
/* Put the ball in the middle. A direction of 1 serves to the right. */
static void reset_ball(float direction)
{
ball.w = BALL_SIZE;
ball.h = BALL_SIZE;
ball.x = (WINDOW_WIDTH - BALL_SIZE) / 2.0f;
ball.y = (WINDOW_HEIGHT - BALL_SIZE) / 2.0f;
ball_speed = BALL_START_SPEED;
ball_vx = direction * ball_speed;
ball_vy = (float)(rand() % 201 - 100); /* -100 to 100 */
}
static void new_game(void)
{
player.w = PADDLE_WIDTH;
player.h = PADDLE_HEIGHT;
player.x = PADDLE_MARGIN;
player.y = (WINDOW_HEIGHT - PADDLE_HEIGHT) / 2.0f;
computer = player;
computer.x = WINDOW_WIDTH - PADDLE_MARGIN - PADDLE_WIDTH;
player_score = 0;
computer_score = 0;
state = STATE_SERVE;
reset_ball(1.0f);
}
/* A hit far from the middle of the paddle sends the ball off at a steep angle. */
static void bounce(const SDL_FRect *paddle, float direction)
{
float paddle_middle = paddle->y + paddle->h / 2.0f;
float ball_middle = ball.y + ball.h / 2.0f;
float offset = (ball_middle - paddle_middle) / (paddle->h / 2.0f);
ball_speed = SDL_min(ball_speed + BALL_SPEED_UP, BALL_MAX_SPEED);
ball_vx = direction * ball_speed;
ball_vy = offset * ball_speed * BALL_STEEPNESS;
}
static void score_point(int *score, float serve_direction)
{
*score += 1;
reset_ball(serve_direction);
state = (*score == WINNING_SCORE) ? STATE_GAME_OVER : STATE_SERVE;
}
static void update_player(float dt)
{
const bool *keys = SDL_GetKeyboardState(NULL);
if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]) {
player.y -= PLAYER_SPEED * dt;
}
if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]) {
player.y += PLAYER_SPEED * dt;
}
player.y = SDL_clamp(player.y, 0.0f, WINDOW_HEIGHT - player.h);
}
/* The computer has a speed limit, and it follows the ball only when the
ball comes toward it. At other times, it goes back to the middle. */
static void update_computer(float dt)
{
float target = (WINDOW_HEIGHT - computer.h) / 2.0f;
float step = COMPUTER_SPEED * dt;
if (ball_vx > 0.0f) {
target = ball.y + ball.h / 2.0f - computer.h / 2.0f;
}
computer.y += SDL_clamp(target - computer.y, -step, step);
computer.y = SDL_clamp(computer.y, 0.0f, WINDOW_HEIGHT - computer.h);
}
static void update_ball(float dt)
{
ball.x += ball_vx * dt;
ball.y += ball_vy * dt;
if (ball.y < 0.0f) {
ball.y = 0.0f;
ball_vy = -ball_vy;
} else if (ball.y + ball.h > WINDOW_HEIGHT) {
ball.y = WINDOW_HEIGHT - ball.h;
ball_vy = -ball_vy;
}
if (ball_vx < 0.0f && overlap(&ball, &player)) {
ball.x = player.x + player.w;
bounce(&player, 1.0f);
} else if (ball_vx > 0.0f && overlap(&ball, &computer)) {
ball.x = computer.x - ball.w;
bounce(&computer, -1.0f);
}
if (ball.x + ball.w < 0.0f) {
score_point(&computer_score, 1.0f);
} else if (ball.x > WINDOW_WIDTH) {
score_point(&player_score, -1.0f);
}
}
static void update(float dt)
{
if (state == STATE_GAME_OVER) {
return;
}
update_player(dt);
update_computer(dt);
if (state == STATE_PLAY) {
update_ball(dt);
}
}
static void press_space(void)
{
if (state == STATE_SERVE) {
state = STATE_PLAY;
} else if (state == STATE_GAME_OVER) {
new_game();
}
}
/* Each point is one small square at the top of the window. */
static void draw_score(int score, float start_x, float step)
{
for (int i = 0; i < score; i++) {
SDL_FRect mark = { start_x + (float)i * step, 16.0f, 12.0f, 12.0f };
SDL_RenderFillRect(renderer, &mark);
}
}
static void draw(void)
{
if (state != STATE_GAME_OVER) {
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
} else if (player_score > computer_score) {
SDL_SetRenderDrawColor(renderer, 24, 84, 60, SDL_ALPHA_OPAQUE);
} else {
SDL_SetRenderDrawColor(renderer, 100, 32, 36, SDL_ALPHA_OPAQUE);
}
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 70, 74, 84, SDL_ALPHA_OPAQUE);
for (float y = 8.0f; y < WINDOW_HEIGHT; y += 32.0f) {
SDL_FRect dash = { WINDOW_WIDTH / 2.0f - 2.0f, y, 4.0f, 16.0f };
SDL_RenderFillRect(renderer, &dash);
}
SDL_SetRenderDrawColor(renderer, 235, 236, 240, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(renderer, &player);
SDL_RenderFillRect(renderer, &computer);
draw_score(player_score, WINDOW_WIDTH / 2.0f - 36.0f, -20.0f);
draw_score(computer_score, WINDOW_WIDTH / 2.0f + 24.0f, 20.0f);
if (state != STATE_GAME_OVER) {
SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(renderer, &ball);
}
SDL_RenderPresent(renderer);
}
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Pong", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
&window, &renderer)) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
new_game();
last_ticks = SDL_GetTicksNS();
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS;
}
if (event->type == SDL_EVENT_KEY_DOWN && !event->key.repeat &&
event->key.scancode == SDL_SCANCODE_SPACE) {
press_space();
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
Uint64 now = SDL_GetTicksNS();
float dt = (float)(now - last_ticks) / (float)SDL_NS_PER_SECOND;
last_ticks = now;
if (dt > MAX_DT) {
dt = MAX_DT;
}
update(dt);
draw();
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
}/* Pong: you against the computer. The first side to reach 5 points wins. */
#include <SDL.h>
#include <stdbool.h>
#include <stdlib.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define MAX_DT 0.05f /* seconds */
#define PADDLE_WIDTH 12.0f
#define PADDLE_HEIGHT 72.0f
#define PADDLE_MARGIN 24.0f
#define BALL_SIZE 12.0f
#define PLAYER_SPEED 340.0f /* pixels per second */
#define COMPUTER_SPEED 250.0f
#define BALL_START_SPEED 280.0f
#define BALL_SPEED_UP 20.0f
#define BALL_MAX_SPEED 450.0f
#define BALL_STEEPNESS 1.0f /* The largest vertical speed, as a part of the ball speed. */
#define WINNING_SCORE 5
typedef enum {
STATE_SERVE, /* The ball waits for the Space key. */
STATE_PLAY,
STATE_GAME_OVER
} GameState;
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static Uint64 last_ticks = 0;
static bool running = true;
static GameState state = STATE_SERVE;
static SDL_FRect player;
static SDL_FRect computer;
static SDL_FRect ball;
static float ball_vx = 0.0f; /* pixels per second */
static float ball_vy = 0.0f;
static float ball_speed = 0.0f;
static int player_score = 0;
static int computer_score = 0;
static bool overlap(const SDL_FRect *a, const SDL_FRect *b)
{
return a->x < b->x + b->w && a->x + a->w > b->x &&
a->y < b->y + b->h && a->y + a->h > b->y;
}
/* Put the ball in the middle. A direction of 1 serves to the right. */
static void reset_ball(float direction)
{
ball.w = BALL_SIZE;
ball.h = BALL_SIZE;
ball.x = (WINDOW_WIDTH - BALL_SIZE) / 2.0f;
ball.y = (WINDOW_HEIGHT - BALL_SIZE) / 2.0f;
ball_speed = BALL_START_SPEED;
ball_vx = direction * ball_speed;
ball_vy = (float)(rand() % 201 - 100); /* -100 to 100 */
}
static void new_game(void)
{
player.w = PADDLE_WIDTH;
player.h = PADDLE_HEIGHT;
player.x = PADDLE_MARGIN;
player.y = (WINDOW_HEIGHT - PADDLE_HEIGHT) / 2.0f;
computer = player;
computer.x = WINDOW_WIDTH - PADDLE_MARGIN - PADDLE_WIDTH;
player_score = 0;
computer_score = 0;
state = STATE_SERVE;
reset_ball(1.0f);
}
/* A hit far from the middle of the paddle sends the ball off at a steep angle. */
static void bounce(const SDL_FRect *paddle, float direction)
{
float paddle_middle = paddle->y + paddle->h / 2.0f;
float ball_middle = ball.y + ball.h / 2.0f;
float offset = (ball_middle - paddle_middle) / (paddle->h / 2.0f);
ball_speed = SDL_min(ball_speed + BALL_SPEED_UP, BALL_MAX_SPEED);
ball_vx = direction * ball_speed;
ball_vy = offset * ball_speed * BALL_STEEPNESS;
}
static void score_point(int *score, float serve_direction)
{
*score += 1;
reset_ball(serve_direction);
state = (*score == WINNING_SCORE) ? STATE_GAME_OVER : STATE_SERVE;
}
static void update_player(float dt)
{
const Uint8 *keys = SDL_GetKeyboardState(NULL);
if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP]) {
player.y -= PLAYER_SPEED * dt;
}
if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN]) {
player.y += PLAYER_SPEED * dt;
}
player.y = SDL_clamp(player.y, 0.0f, WINDOW_HEIGHT - player.h);
}
/* The computer has a speed limit, and it follows the ball only when the
ball comes toward it. At other times, it goes back to the middle. */
static void update_computer(float dt)
{
float target = (WINDOW_HEIGHT - computer.h) / 2.0f;
float step = COMPUTER_SPEED * dt;
if (ball_vx > 0.0f) {
target = ball.y + ball.h / 2.0f - computer.h / 2.0f;
}
computer.y += SDL_clamp(target - computer.y, -step, step);
computer.y = SDL_clamp(computer.y, 0.0f, WINDOW_HEIGHT - computer.h);
}
static void update_ball(float dt)
{
ball.x += ball_vx * dt;
ball.y += ball_vy * dt;
if (ball.y < 0.0f) {
ball.y = 0.0f;
ball_vy = -ball_vy;
} else if (ball.y + ball.h > WINDOW_HEIGHT) {
ball.y = WINDOW_HEIGHT - ball.h;
ball_vy = -ball_vy;
}
if (ball_vx < 0.0f && overlap(&ball, &player)) {
ball.x = player.x + player.w;
bounce(&player, 1.0f);
} else if (ball_vx > 0.0f && overlap(&ball, &computer)) {
ball.x = computer.x - ball.w;
bounce(&computer, -1.0f);
}
if (ball.x + ball.w < 0.0f) {
score_point(&computer_score, 1.0f);
} else if (ball.x > WINDOW_WIDTH) {
score_point(&player_score, -1.0f);
}
}
static void update(float dt)
{
if (state == STATE_GAME_OVER) {
return;
}
update_player(dt);
update_computer(dt);
if (state == STATE_PLAY) {
update_ball(dt);
}
}
static void press_space(void)
{
if (state == STATE_SERVE) {
state = STATE_PLAY;
} else if (state == STATE_GAME_OVER) {
new_game();
}
}
/* Each point is one small square at the top of the window. */
static void draw_score(int score, float start_x, float step)
{
for (int i = 0; i < score; i++) {
SDL_FRect mark = { start_x + (float)i * step, 16.0f, 12.0f, 12.0f };
SDL_RenderFillRectF(renderer, &mark);
}
}
static void draw(void)
{
if (state != STATE_GAME_OVER) {
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
} else if (player_score > computer_score) {
SDL_SetRenderDrawColor(renderer, 24, 84, 60, SDL_ALPHA_OPAQUE);
} else {
SDL_SetRenderDrawColor(renderer, 100, 32, 36, SDL_ALPHA_OPAQUE);
}
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 70, 74, 84, SDL_ALPHA_OPAQUE);
for (float y = 8.0f; y < WINDOW_HEIGHT; y += 32.0f) {
SDL_FRect dash = { WINDOW_WIDTH / 2.0f - 2.0f, y, 4.0f, 16.0f };
SDL_RenderFillRectF(renderer, &dash);
}
SDL_SetRenderDrawColor(renderer, 235, 236, 240, SDL_ALPHA_OPAQUE);
SDL_RenderFillRectF(renderer, &player);
SDL_RenderFillRectF(renderer, &computer);
draw_score(player_score, WINDOW_WIDTH / 2.0f - 36.0f, -20.0f);
draw_score(computer_score, WINDOW_WIDTH / 2.0f + 24.0f, 20.0f);
if (state != STATE_GAME_OVER) {
SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
SDL_RenderFillRectF(renderer, &ball);
}
SDL_RenderPresent(renderer);
}
static void frame(void)
{
SDL_Event event;
Uint64 now = SDL_GetPerformanceCounter();
float dt = (float)(now - last_ticks) / (float)SDL_GetPerformanceFrequency();
last_ticks = now;
if (dt > MAX_DT) {
dt = MAX_DT;
}
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
}
if (event.type == SDL_KEYDOWN && !event.key.repeat &&
event.key.keysym.scancode == SDL_SCANCODE_SPACE) {
press_space();
}
}
update(dt);
draw();
}
int main(int argc, char *argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return 1;
}
if (SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer) != 0) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_SetWindowTitle(window, "Pong");
SDL_RenderSetVSync(renderer, 1);
new_game();
last_ticks = SDL_GetPerformanceCounter();
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(frame, 0, 1);
#else
while (running) {
frame();
}
#endif
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}Use constants
The numbers that control the game are at the top of the file, with names. To change the game, you change these lines. A name like BALL_MAX_SPEED also tells the reader what the number means.
Game state
The game has three states. The ball waits for you, the ball is in play, or the game is over. An enum gives each state a name:
typedef enum {
STATE_SERVE, /* The ball waits for the Space key. */
STATE_PLAY,
STATE_GAME_OVER
} GameState;The program does different work in each state. The update function shows this:
static void update(float dt)
{
if (state == STATE_GAME_OVER) {
return;
}
update_player(dt);
update_computer(dt);
if (state == STATE_PLAY) {
update_ball(dt);
}
}The paddles can move in the serve state, but the ball cannot. Nothing moves after the game ends.
This pattern is a state machine (a program that is in one named state at a time). Most games use one.
Update, then draw
update(dt);
draw();
return SDL_APP_CONTINUE; update(dt);
draw();
}Each frame has two steps. The update function changes the game data, and it draws nothing. The draw function shows the game data, and it changes nothing. This split keeps the rules of the game away from the drawing code, so you can change one without the other.
The computer player
float target = (WINDOW_HEIGHT - computer.h) / 2.0f;
float step = COMPUTER_SPEED * dt;
if (ball_vx > 0.0f) {
target = ball.y + ball.h / 2.0f - computer.h / 2.0f;
}
computer.y += SDL_clamp(target - computer.y, -step, step);The computer moves toward a target position, and SDL_clamp limits each step. While the ball moves away, the target is the middle of the window. While the ball comes closer, the target puts the paddle in line with the ball.
Two limits make the game fair. The paddle has a maximum speed, and it does not follow a ball that moves away. A fast ball at a steep angle can reach the edge of the window before the paddle does. Without these limits, the computer almost never misses, and the game does not end.
The ball
The top and bottom walls work like the last lesson. The paddle test has one more condition:
if (ball_vx < 0.0f && overlap(&ball, &player)) {
ball.x = player.x + player.w;
bounce(&player, 1.0f);The test ball_vx < 0.0f means that the ball moves toward your paddle. The program also puts the ball on the outside of the paddle. Together, these two lines stop a second hit on the next frame.
The ball gets its angle from the hit position:
float offset = (ball_middle - paddle_middle) / (paddle->h / 2.0f);The value of offset is 0 at the middle of the paddle, and about 1 or -1 at the ends. A hit near the end of the paddle sends the ball away at a steep angle. This gives you control over the ball.
Each hit adds to the speed, up to BALL_MAX_SPEED. The maximum obeys the rule from the last lesson. At 450 pixels each second and a MAX_DT of 0.05, the longest step is 22.5 pixels. This is less than the 24 pixels of the paddle and the ball together.
Keep the score
static void score_point(int *score, float serve_direction)
{
*score += 1;
reset_ball(serve_direction);
state = (*score == WINNING_SCORE) ? STATE_GAME_OVER : STATE_SERVE;
}The function takes a pointer, so one function can change either score. The call score_point(&computer_score, 1.0f) adds a point for the computer. The ball then serves toward the side that won the point.
The score uses small squares, because the lesson Draw text comes later.
Try this
- Change
WINNING_SCOREandCOMPUTER_SPEED, and find a good balance. - Replace the computer with a second player on the Up and Down arrows.
- Make the paddles shorter as the score goes up.
- Add a pause state, and use the P key to enter it and leave it.
What you learned
- An
enumand a state variable keep track of what the game is doing. - Separate
updateanddrawfunctions keep the rules apart from the drawing. - Named constants make a game easy to tune.