# Capstone: a 2D game The second capstone is a small but complete 2D game: a paddle, a ball, a score, a game loop that runs at a steady frame rate, and keyboard control. It is built on [Prima](gui-toolkits.md) again, because Prima gives us the three things a game needs (a timer for the loop, keyboard events, and fast drawing) in a maintained package that runs today. At the end of the chapter we look honestly at the alternative, a dedicated game library like SDL2, and why reaching it well is still frontier work for pperl’s FFI. A game is a loop: read input, update the world, draw the world, wait for the next frame, repeat. Everything else is detail. We build that loop first, then hang a game on it. ## The game loop: a timer A realtime loop needs a heartbeat. Prima provides `Prima::Timer`, which fires an `onTick` handler on an interval; setting it to roughly 16 milliseconds gives about 60 frames per second. Each tick updates the world and asks the window to repaint. ```perl use strict; use warnings; use Prima qw(Application); my $W = 480; my $H = 360; # the world state, plain data my %game = ( ball => { x => 240, y => 180, dx => 3, dy => 3, r => 8 }, paddle => { x => 200, w => 80, h => 12 }, score => 0, over => 0, ); ``` The entire game world is a plain hash. That is deliberate: the update logic is ordinary Perl on ordinary data, and the drawing reads the same data. Nothing about the loop is graphics-specific until the draw step. ## Update: moving the world Each tick advances the ball, bounces it off the walls and the paddle, and ends the game if the ball passes the paddle. This is pure arithmetic on the world hash, no drawing: ```perl sub update { return if $game{over}; my $b = $game{ball}; $b->{x} += $b->{dx}; $b->{y} += $b->{dy}; # bounce off left, right, and top walls $b->{dx} = -$b->{dx} if $b->{x} - $b->{r} < 0 || $b->{x} + $b->{r} > $W; $b->{dy} = -$b->{dy} if $b->{y} - $b->{r} < 0; # paddle collision near the bottom my $p = $game{paddle}; my $paddle_y = $H - 30; if ($b->{y} + $b->{r} >= $paddle_y && $b->{x} >= $p->{x} && $b->{x} <= $p->{x} + $p->{w}) { $b->{dy} = -abs($b->{dy}); # always bounce upward $game{score}++; } # missed the paddle: game over $game{over} = 1 if $b->{y} - $b->{r} > $H; } ``` The collision test is the axis-aligned overlap from the [foundations](foundations.md) coordinate system: the ball has crossed the paddle’s row and lies within its horizontal span. Bouncing is negating a velocity component. A whole genre of games is this and not much more. ## Draw: rendering the world Drawing reads the world hash and paints it. Prima calls `onPaint` with a canvas; we clear it, then draw the paddle, the ball, and the score using the primitives from the [GUI chapter](gui-toolkits.md): ```perl sub draw { my ($canvas) = @_; $canvas->color(cl::Black); $canvas->bar(0, 0, $W, $H); # clear to black my $p = $game{paddle}; $canvas->color(cl::White); $canvas->bar($p->{x}, $H - 30, $p->{x} + $p->{w}, $H - 30 + $p->{h}); my $b = $game{ball}; $canvas->color(cl::Yellow); $canvas->fill_ellipse($b->{x}, $b->{y}, $b->{r} * 2, $b->{r} * 2); $canvas->color(cl::White); $canvas->text_out("Score: $game{score}", 10, $H - 20); $canvas->text_out('GAME OVER', $W / 2 - 40, $H / 2) if $game{over}; } ``` `text_out` draws a string; `fill_ellipse` and `bar` are the ball and the paddle. Clearing to black every frame and redrawing everything is the immediate-mode style: the screen is a function of the world state, recomputed each frame, which is simple to reason about and fast enough for a game this size. ## Input: the keyboard The paddle follows the arrow keys. Prima delivers key presses through an `onKeyDown` handler, which we use to slide the paddle left and right, clamped to the window: ```perl sub on_key { my ($self, $code, $key, $mod) = @_; my $p = $game{paddle}; $p->{x} -= 25 if $key == kb::Left; $p->{x} += 25 if $key == kb::Right; $p->{x} = 0 if $p->{x} < 0; $p->{x} = $W - $p->{w} if $p->{x} > $W - $p->{w}; } ``` The `kb::Left` and `kb::Right` constants name the arrow keys. Clamping keeps the paddle on screen. For smooth continuous motion a game often tracks which keys are currently held and moves in the update step; a step-per-press is simpler and fine here. ## Wiring it together The window ties the three handlers to the loop. The timer drives update and repaint; the paint handler draws; the key handler steers: ```perl my $window = Prima::MainWindow->new( text => 'pperl bounce', size => [$W, $H], onPaint => sub { my ($self, $canvas) = @_; draw($canvas); }, onKeyDown => \&on_key, ); $window->insert(Timer => timeout => 16, # ~60 fps onTick => sub { update(); $window->repaint; }, )->start; Prima->run; ``` Assemble the pieces into one file and run it with `pperl`. A window opens with a bouncing ball and a paddle you steer with the arrow keys; each successful bounce scores a point, and missing ends the game. Every part traces through the guide: the loop is a timer, the world is plain data, the update is arithmetic on coordinates from [foundations](foundations.md), and the draw step is [Prima primitives](gui-toolkits.md). ## The frontier: SDL2 and dedicated game libraries Prima runs this game well, and for many 2D games it is the right tool. But a serious game usually wants a dedicated library: SDL2, for hardware -accelerated sprites, sound, gamepads, and a tuned event loop. Here the guide has to be honest about where pperl stands. The Perl SDL story is thin. The old `SDL` distribution binds SDL 1.2, which is end-of-life, and it is itself effectively unmaintained. There is no solid, maintained SDL2 binding on CPAN; the one FFI-based attempt is alpha-grade, with an incomplete test suite and shaky threading. So SDL2 is exactly the kind of modern C library the [FFI thread](ffi-graphics-primer.md) exists to reach, and it is also exactly where the current FFI frontier bites. Some of SDL2 is reachable with raw [`Peta::FFI`](ffi-graphics-primer.md) calls today. Creating a window and a renderer returns opaque handles (pointers, so integers), and many calls take only scalars: ```perl use Peta::FFI qw(dlopen call dlclose); my $sdl = dlopen("libSDL2.so"); call($sdl, "SDL_Init", "(L)i", 0x20); # SDL_INIT_VIDEO my $win = call($sdl, "SDL_CreateWindow", "(piiiiL)l", "pperl", 100, 100, 480, 360, 4); # returns a handle # ... and so on for the renderer, using the returned handle ``` That much works: scalar arguments, string arguments, and opaque handles returned as integers are precisely what the raw type codes cover. But a real game loop needs the part that does not yet work. `SDL_PollEvent` fills an `SDL_Event`, which is a **union** of many struct shapes, and the raw type codes cannot describe a struct, let alone a union. Reading which key was pressed means decoding that struct’s bytes, which is what the [Level 2 data-description layer](ffi-graphics-primer.md) is being built to do. And a fully idiomatic binding would want callbacks, which the raw layer also does not reach. So the honest position is this: a **complete, comfortable** SDL2 binding on pperl is frontier work, waiting on the FFI’s struct and callback support. It is reachable, it is on the roadmap, and it is exactly the kind of binding the [Cairo showcase](ffi-cairo-showcase.md) pattern will produce once the data-description layer lands. Until then, Prima is the capstone’s honest, running answer, and the SDL2 route is a real but partial path clearly marked as frontier. ## Where the guide ends You have now built two programs that run: a drawing application and a game, both on the maintained toolkit stack, with the FFI thread woven in where it earns its place and flagged where the road currently ends. The [final chapter](whats-next.md) draws that map explicitly: what a second version would add, and which frontiers, the third dimension chief among them, are deliberately left for the sequel.