Banjo API 1.0.0-rc.2
Low-level C99 game development API
Loading...
Searching...
No Matches
pong.c

Capstone tutorial - a complete two-player Pong, tying together the subsystems you've seen so far: window, input, drawing, and timing.

Capstone tutorial - a complete two-player Pong, tying together the subsystems you've seen so far: window, input, drawing, and timing.This is the finale of the tutorial series. By the end of audio_pcm.c you knew how to open a window, draw to it, react to input, and time your frames. pong.c is the first tutorial that does all of those things together in a real (if minimal) game.

It is deliberately kept small. It stays with the simple variable-timestep loop from the earlier tutorials rather than the fixed-step API (via a bj_app_fixed_step_fn callback). Reach for fixed-step when determinism across machines matters (networked play, physics that must replay identically); see physics_particle.c and physics_kinematics.c. Pong does not need it, and the simpler loop keeps the capstone readable.

What you'll learn

  • How to structure a complete game: one state struct, a fixed update / draw split, and the lifecycle callbacks holding the pieces.
  • How to pace a real-time loop with one line, bj_set_frame_rate, instead of scattering sleeps through your code. Without a cap, bj_run_app calls the step callback as fast as it can and burns a full core for a game that only needs about 60 updates per second.
  • How to do frame-rate-independent motion using bj_step_delay_stopwatch as your delta-time source, with a clamp to keep the ball from teleporting after a pause.
  • How to poll continuous-state input with bj_get_key (the right tool for paddle control; callbacks would only give you edge events).

Controls

  • Left paddle: W (up) and S (down).
  • Right paddle: Up and Down arrows.
  • Quit: ESC.

Walkthrough

1. One state struct, passed through the lifecycle

Earlier tutorials used file-scope globals (window, data, ...). That works for one-screen demos; it scales badly. Pong gathers everything into pong_t, declared zero-initialised on the stack in main (pong_t pong = {0}), and passes a pointer through the setup, step, and teardown callbacks via the App API's user_data parameter (registered with each bj_set_*_callback call). The zero-init matters: bj_stopwatch's "zero-init = valid" property means we don't need an explicit reset call.

2. Frame pacing with no effort

Pong uses the default frame rate (60 Hz), so there's nothing to set. bj_run_app measures how long each iteration took and sleeps off just the time left to reach 60 iterations per second, so the loop yields the CPU instead of melting a core. Non-default rates can be set inside the setup callback with bj_set_frame_rate.

3. Delta time, the right way

bj_step_delay_stopwatch returns the seconds elapsed since the last call and advances the internal step in one go. Every motion computation is then position += velocity * dt, so the game runs at the same speed whether the frame is 1 ms or 16 ms.

The clamped_dt line caps the step at 50 ms. Without it, alt-tabbing away for a few seconds (or any pause longer than a few frames) would resume with a giant dt and teleport the ball through a paddle. Cap-and-continue is the standard remedy.

4. Polled input vs. callbacks

start.c and event_callbacks.c used the callback style, perfect for one-shot events ("ESC was pressed"). For paddles you want the opposite: "is W *currently held*?" bj_get_key answers exactly that. We still use the ESC callback (via bj_close_on_escape), so this tutorial shows the two styles cooperating: callbacks for discrete events, polling for continuous state.

5. Cleanup order

Reverse of init: window, then bj_end. The state struct lives on the stack in main, so there is nothing to free; once bj_run_app returns, main drops it automatically.

What's next

You've finished the tutorial series. From here you can:

  • Read any of the single-API demos in examples/ that look interesting (each is now slotted into its topic; visit Bitmap, Audio, Random, Network, …).
  • Read the Windows and Audio topics for the rationale behind Banjo's runtime backend selection.
  • Start your own project: the FetchContent recipe in Using Banjo has you running in under a minute.
#include <banjo/app.h>
#include <banjo/main.h>
#include <banjo/bitmap.h>
#include <banjo/draw.h>
#include <banjo/event.h>
#include <banjo/log.h>
#include <banjo/memory.h>
#include <banjo/rect.h>
#include <banjo/system.h>
#include <banjo/time.h>
#include <banjo/window.h>
#define W 800
#define H 600
#define PAD_W 16
#define PAD_H 80
#define PAD_MARGIN 32
#define BALL_R 8
#define PAD_SPEED 360.0f /* pixels per second */
#define BALL_SPEED 280.0f /* pixels per second (initial) */
#define BOUNCE_SPEEDUP 1.05f /* paddle hit accelerates the ball */
#define MAX_DT 0.05f /* clamp delta to avoid teleporting */
typedef struct {
float pad_left_y;
float pad_right_y;
float ball_x, ball_y;
float ball_vx, ball_vy;
static void reset_ball(pong_t* p, int last_winner) {
p->ball_x = (float)W * 0.5f;
p->ball_y = (float)H * 0.5f;
p->ball_vx = (last_winner >= 0 ? +BALL_SPEED : -BALL_SPEED);
p->ball_vy = (last_winner & 1) ? BALL_SPEED * 0.6f
: -BALL_SPEED * 0.6f;
}
static void update(pong_t* p, float dt) {
/* --- Paddle input: continuous-state polling -------------------- */
/* --- Clamp paddles to the playfield ---------------------------- */
if (p->pad_left_y < 0) p->pad_left_y = 0;
if (p->pad_left_y > H - PAD_H) p->pad_left_y = (float)(H - PAD_H);
if (p->pad_right_y < 0) p->pad_right_y = 0;
if (p->pad_right_y > H - PAD_H) p->pad_right_y = (float)(H - PAD_H);
/* --- Ball ------------------------------------------------------ */
p->ball_x += p->ball_vx * dt;
p->ball_y += p->ball_vy * dt;
/* Top / bottom walls bounce */
if (p->ball_y < BALL_R) {
p->ball_y = BALL_R;
p->ball_vy = -p->ball_vy;
}
if (p->ball_y > H - BALL_R) {
p->ball_y = (float)(H - BALL_R);
p->ball_vy = -p->ball_vy;
}
/* Left paddle */
if (p->ball_vx < 0 &&
p->ball_y > p->pad_left_y &&
p->ball_y < p->pad_left_y + PAD_H) {
p->ball_x = (float)(PAD_MARGIN + PAD_W + BALL_R);
}
/* Right paddle */
if (p->ball_vx > 0 &&
p->ball_x + BALL_R > W - PAD_MARGIN - PAD_W &&
p->ball_y > p->pad_right_y &&
p->ball_y < p->pad_right_y + PAD_H) {
p->ball_x = (float)(W - PAD_MARGIN - PAD_W - BALL_R);
}
/* Score (ball escaped past a paddle): reset toward the loser */
if (p->ball_x < -BALL_R) {
reset_ball(p, +1);
} else if (p->ball_x > W + BALL_R) {
reset_ball(p, -1);
}
}
static void draw(pong_t* p, bj_bitmap* fb) {
const uint32_t white = bj_make_bitmap_pixel(fb, 0xFF, 0xFF, 0xFF);
const uint32_t gray = bj_make_bitmap_pixel(fb, 0x40, 0x40, 0x40);
/* Dashed centre line */
for (int16_t y = 0; y < H; y = (int16_t)(y + 24)) {
bj_rect dash = { .x = (int16_t)(W / 2 - 2), .y = y, .w = 4, .h = 12 };
bj_draw_filled_rectangle(fb, &dash, gray);
}
/* Paddles */
bj_rect lpad = { .x = PAD_MARGIN, .y = (int16_t)p->pad_left_y,
.w = PAD_W, .h = PAD_H };
bj_rect rpad = { .x = (int16_t)(W - PAD_MARGIN - PAD_W), .y = (int16_t)p->pad_right_y,
.w = PAD_W, .h = PAD_H };
bj_draw_filled_rectangle(fb, &lpad, white);
bj_draw_filled_rectangle(fb, &rpad, white);
/* Ball */
bj_draw_filled_circle(fb, (int)p->ball_x, (int)p->ball_y, BALL_R, white);
}
static void on_draw(
struct bj_window* window,
struct bj_render_target* target,
const struct bj_rect* dirty,
void* user_data
) {
(void)window; (void)dirty;
draw((pong_t*)user_data, bj_render_target_bitmap(target));
}
static void* setup(struct bj_app* app, void* init_data) {
(void)init_data;
pong_t* p = bj_calloc(sizeof(pong_t));
bj_err("pong: video init failed");
bj_free(p);
bj_quit_app(app, 1);
return 0;
}
p->window = bj_bind_window("Pong - W/S vs Up/Down (ESC to quit)",
100, 100, W, H, 0, 0);
p->pad_left_y = (float)(H - PAD_H) * 0.5f;
p->pad_right_y = (float)(H - PAD_H) * 0.5f;
reset_ball(p, 0);
return p;
}
static void step(struct bj_app* app, struct bj_tick_info tick, void* user_data) {
(void)tick;
pong_t* p = (pong_t*)user_data;
const float dt = (float)bj_step_delay_stopwatch(&p->sw);
const float clamped_dt = dt > MAX_DT ? MAX_DT : dt;
update(p, clamped_dt);
bj_quit_app(app, 0);
}
}
static void teardown(struct bj_app* app, void* user_data) {
(void)app;
pong_t* p = (pong_t*)user_data;
bj_end();
bj_free(p);
}
int main(int argc, char* argv[]) {
(void)argc; (void)argv;
return bj_run_app(setup, step, 0, teardown, 0);
}
Application lifecycle: callback-driven setup, step, and teardown.
int main(int argc, char *argv[])
Definition audio_pcm.c:177
static void step(struct bj_app *app, struct bj_tick_info tick, void *user_data)
Definition audio_pcm.c:144
static void teardown(struct bj_app *app, void *user_data)
Definition audio_pcm.c:170
static void * setup(struct bj_app *app, void *init_data)
Definition audio_pcm.c:107
Header file for Bitmap type.
static void on_draw(struct bj_window *w, struct bj_render_target *target, const struct bj_rect *dirty, void *user_data)
Definition bitmap_blit.c:32
bj_window * window
Definition bitmap_blit.c:24
Header file for Bitmap drawing functions.
void draw(bj_bitmap *bmp)
Definition drawing_2d.c:133
Sytem event management API.
int bj_run_app(bj_app_setup_fn setup, bj_app_step_fn step, bj_app_fixed_step_fn fixed_step, bj_app_teardown_fn teardown, void *init_data)
Drive the application lifecycle.
void bj_quit_app(struct bj_app *app, int exit_code)
Signal the given application to exit on the next iteration.
Timing snapshot handed to the step and fixed-step callbacks.
Definition app.h:106
void bj_clear_bitmap(struct bj_bitmap *bitmap)
Fills the entire bitmap with the clear colour.
uint32_t bj_make_bitmap_pixel(struct bj_bitmap *bitmap, uint8_t red, uint8_t green, uint8_t blue)
Returns an opaque value representing a pixel colour, given its RGB composition.
struct bj_render_target bj_render_target
Definition api.h:346
struct bj_bitmap bj_bitmap
Definition api.h:328
struct bj_window bj_window
Definition api.h:354
void bj_draw_filled_rectangle(struct bj_bitmap *bitmap, const struct bj_rect *area, uint32_t pixel)
Draws a filled rectangle in the given bitmap.
void bj_draw_filled_circle(struct bj_bitmap *bitmap, int cx, int cy, int radius, uint32_t color)
Draw a filled circle onto a bitmap.
void bj_dispatch_events(void)
Poll and dispatch all pending events.
void bj_close_on_escape(struct bj_window *window, const struct bj_key_event *event, void *user_data)
Handle the ESC key to close a window.
bj_key_callback_fn bj_set_key_callback(bj_key_callback_fn callback, void *user_data)
Set the global callback for keyboard key events.
@ BJ_KEY_S
S key.
Definition event.h:226
@ BJ_KEY_DOWN
Down arrow key.
Definition event.h:189
@ BJ_KEY_W
W key.
Definition event.h:230
@ BJ_KEY_UP
Up arrow key.
Definition event.h:187
@ BJ_PRESS
The key or button was pressed.
Definition event.h:392
#define bj_err(...)
Log a message using the BJ_LOG_ERROR level.
Definition log.h:169
Axis-aligned rectangle: a top-left corner plus a width and height.
Definition rect.h:33
void * bj_calloc(size_t size)
Allocate size bytes of zero-initialised memory.
void bj_free(void *memory)
Free a previously allocated memory block.
bj_bool bj_begin(int systems, struct bj_error **error)
Initialises the system.
void bj_end(void)
De-initialises the system.
@ BJ_VIDEO_SYSTEM
Definition system.h:81
double bj_step_delay_stopwatch(struct bj_stopwatch *stopwatch)
Steps the stopwatch and returns the delay since the previous step.
Structure representing a simple stopwatch.
Definition time.h:150
void bj_set_draw_callback(struct bj_window *window, bj_window_draw_fn fn, void *user_data)
Register the redraw callback for window.
static void bj_invalidate_window(struct bj_window *window)
Mark the whole window as needing a repaint.
Definition window.h:470
struct bj_window * bj_bind_window(const char *title, uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t flags, struct bj_error **error)
Create a new struct bj_window with the specified attributes.
int bj_get_key(const struct bj_window *window, int key)
Query the current state of a key for a given window.
struct bj_bitmap * bj_render_target_bitmap(struct bj_render_target *target)
Reach the software framebuffer behind a render target.
bj_bool bj_should_close_window(struct bj_window *window)
Get the close flag state of a window.
void bj_unbind_window(struct bj_window *window)
Deletes a struct bj_window object and releases associated memory.
#define BALL_R
Logging utility functions.
Portable main() replacement with platform-aware entry shim.
All memory-related functions, including custom allocators.
static void reset_ball(size_t at)
static void update(bj_real dt)
float ball_vx
Definition pong.c:134
#define PAD_W
Definition pong.c:118
#define BOUNCE_SPEEDUP
Definition pong.c:124
#define BALL_SPEED
Definition pong.c:123
bj_stopwatch sw
Definition pong.c:129
float pad_right_y
Definition pong.c:132
#define PAD_MARGIN
Definition pong.c:120
bj_window * window
Definition pong.c:128
#define W
Definition pong.c:116
float ball_x
Definition pong.c:133
float ball_y
Definition pong.c:133
float ball_vy
Definition pong.c:134
float pad_left_y
Definition pong.c:131
#define PAD_H
Definition pong.c:119
#define H
Definition pong.c:117
#define MAX_DT
Definition pong.c:125
#define PAD_SPEED
Definition pong.c:122
Definition pong.c:127
Axis-aligned rectangle in pixel coordinates.
Header file for system interactions.
Header file for time manipulation utilities.
Header file for bj_window type.