# 20. Tutorial: GALAGA — a C engine with a BASIC brain This chapter and [Chapter 21](21-tutorial-galaga-enemies.md) build a GALAGA-style fixed shooter from an empty file. The engine — window, starfield, bullets, collision, score, screens — is C on libakgl. The enemies think in BASIC: one script of `DEF` functions is called once per enemy per frame, and it reads and writes the engine's own structures with no marshalling in either direction. This chapter builds the engine and proves the boundary works; the next one fills in the data structures and the AI. The split is the point. Everything mechanical stays compiled, and everything an enemy *decides* is a text file you can edit and re-run without rebuilding. It is an academic exercise in *how* such an embed is done, not a claim that it is the best way to write a GALAGA. This is what the two chapters build: ![A full wave: four green bosses, two rows of butterflies, bees still streaming into the grid, the player firing](images/galaga-wave.png) The finished program is [`examples/galaga/`](../examples/galaga/): four C files, one `galaga.bas`, and the assets. You do not need it to follow along, but it is the same program assembled. ```sh norun $ cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON $ cmake --build build-akgl --target akbasic_example_galaga $ ./build-akgl/akbasic_example_galaga ``` | Key | Does | |---|---| | left / right | move the ship | | space | fire — two shots on screen at a time, the classic rule | | return | choose a menu entry | ## What you will do - **[Step 1](#step-1-open-a-window)** — open a window, in the one startup order that works - **[Step 2](#step-2-scatter-a-starfield)** — scatter a starfield and scroll it, with no parallax machinery at all - **[Step 3](#step-3-put-a-ship-on-screen)** — put a ship on screen from a sprite and a character file, and drive it from the keyboard - **[Step 4](#step-4-shots-and-collision)** — spawn shots from the actor heap and collide them by hand - **[Step 5](#step-5-boot-the-interpreter)** — link the interpreter in, load a script of definitions, and call one from C - **[Step 6](#step-6-the-update-hook)** — replace an actor's update hook so its every frame is a BASIC call - **[Step 7](#step-7-first-light)** — watch one enemy move under BASIC control, and read the same numbers from both sides - **[Step 8](#step-8-screens)** — add the title, game over and victory screens - **[Step 9](#step-9-run-it-headless)** — run the whole game headless, so CI can play it every night Each step compiles and runs. The C fragments quote the finished example; the file layout there — `main.c` for the harness, `script.c` for the boundary, `enemies.c` and `player.c` for the actors — is a good one to copy. --- ## Step 1: Open a window **Goal: a black window with a title, from the canonical startup order.** libakgl has one startup sequence that works, documented at the top of its `include/akgl/game.h` and walked through in its own tutorial (libakgl docs/20-tutorial-sidescroller.md). The order matters twice: the screen properties are read by the renderer, so they must be set before it exists, and `akgl_game_init()` does **not** install a physics backend, so the application must. ```c wrap=galagatypes requires=akgl static akerr_ErrorContext *startup(void) { PREPARE_ERROR(errctx); PASS(errctx, aksl_strncpy((char *)&akgl_game.name, sizeof(akgl_game.name), "akbasic galaga tutorial", sizeof(akgl_game.name) - 1)); PASS(errctx, aksl_strncpy((char *)&akgl_game.version, sizeof(akgl_game.version), "1.0.0", sizeof(akgl_game.version) - 1)); PASS(errctx, aksl_strncpy((char *)&akgl_game.uri, sizeof(akgl_game.uri), "net.aklabs.akbasic.galaga", sizeof(akgl_game.uri) - 1)); PASS(errctx, akgl_game_init()); PASS(errctx, akgl_set_property("game.screenwidth", "1280")); PASS(errctx, akgl_set_property("game.screenheight", "960")); PASS(errctx, akgl_render_2d_init(akgl_renderer)); FAIL_ZERO_RETURN( errctx, SDL_SetRenderLogicalPresentation( akgl_renderer->sdl_renderer, 1280, 960, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE), AKGL_ERR_SDL, "%s", SDL_GetError() ); akgl_camera->x = 0.0f; akgl_camera->y = 0.0f; akgl_camera->w = 1280.0f; akgl_camera->h = 960.0f; PASS(errctx, akgl_physics_init_null(akgl_physics)); SUCCEED_RETURN(errctx); } ``` Three of those lines deserve their reasons. **The view is 1280x960 because the artwork is ~100 pixels wide.** libakgl draws a sprite at the sprite's own size — `akgl_Actor.scale` is overwritten every frame, so there is no way to draw one smaller (libakgl docs/12-actors.md) — and a ten-column formation of 100-pixel ships needs 1120 pixels plus margins. The view is sized to the art rather than the art resized to a view. **`akgl_physics_init_null()` is not optional.** Skip it and the first `akgl_game_update()` calls through a NULL `simulate` pointer. Null physics accepts every call and moves nothing, which is exactly right here: whatever writes `x` and `y` directly is the mover, and in this game that will be BASIC. **Error handling is the house protocol.** Every function returns `akerr_ErrorContext *`, `PASS` propagates, `ATTEMPT`/`CATCH`/`CLEANUP` brackets anything that must unwind. libakgl's docs/04-errors.md teaches it; this chapter just uses it. The frame loop is the standard bracket, with one addition you will meet in Step 6 — for now, events in, world drawn, frame out: ```c wrap=galagahost requires=akgl while ( SDL_PollEvent(&event) == true ) { CATCH(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event)); } CATCH(errctx, akgl_renderer->frame_start(akgl_renderer)); CATCH(errctx, akgl_game_update(NULL)); CATCH(errctx, akgl_renderer->frame_end(akgl_renderer)); ``` `akgl_game_update(NULL)` is update-every-actor, step-the-physics, draw-the-world. It neither clears nor presents; the `frame_start` and `frame_end` calls own that. ## Step 2: Scatter a starfield **Goal: a scrolling two-depth starfield, from an array and one draw call.** No parallax facility exists in libakgl and none is needed. A fixed array of stars, advanced per frame and drawn with `akgl_draw_point()` between `frame_start` and `akgl_game_update()`, is the whole feature. Two speed bands give the depth for free — the slow band reads as far away: ```c wrap=galagatypes requires=akgl #define GALAGA_STARS 96 static struct { float x; float y; float speed; Uint8 bright; } STARS[GALAGA_STARS]; static akerr_ErrorContext *starfield_draw(float dt) { SDL_Color color = { 255, 255, 255, 255 }; int i = 0; PREPARE_ERROR(errctx); for ( i = 0; i < GALAGA_STARS; i++ ) { STARS[i].y += STARS[i].speed * dt; if ( STARS[i].y > 960.0f ) { STARS[i].y -= 960.0f; } color.r = STARS[i].bright; color.g = STARS[i].bright; color.b = STARS[i].bright; PASS(errctx, akgl_draw_point(akgl_renderer, STARS[i].x, STARS[i].y, color)); } SUCCEED_RETURN(errctx); } ``` Seed the array once at startup — even indexes slow and dim (speed 40, bright 110), odd indexes fast and bright (speed 110, bright 220) — and the effect is done. A point is exactly one pixel (libakgl docs/09-drawing.md). ## Step 3: Put a ship on screen **Goal: a player actor, drawn from a character file, moving on key input.** The art is Kenney's Space Shooter pack, CC0, used byte for byte — see [`examples/galaga/assets/art/PROVENANCE.md`](../examples/galaga/assets/art/PROVENANCE.md) for what each file is. An actor gets its looks from a **character**, which maps actor state words to **sprites** (libakgl docs/10 and 12). Both are JSON; load sprites first, because a character names its sprites and a character loaded first fails on the first name it cannot find. The spawn is four decisions after the two boilerplate calls: ```c wrap=galagagame requires=akgl static akerr_ErrorContext *galaga_player_spawn(void) { akgl_Actor *player = NULL; PREPARE_ERROR(errctx); PASS(errctx, akgl_heap_next_actor(&player)); PASS(errctx, akgl_actor_initialize(player, "player")); PASS(errctx, akgl_actor_set_character(player, "galaga_player")); /* AFTER initialize: it resets all seven hooks. */ player->updatefunc = &player_update; player->movement_controls_face = false; player->state = AKGL_ACTOR_STATE_ALIVE; player->visible = true; player->x = 590.0f; player->y = 860.0f; galaga_game.player = player; SUCCEED_RETURN(errctx); } ``` Each of the four lines under the comment closes a trap: - **`updatefunc` after `akgl_actor_initialize()`**, never before — initialize installs all seven default hooks, and a hook set first is a hook reset. - **`movement_controls_face = false`.** The default facing logic edits the state word, a character mapping matches the **whole** word, and an actor whose state matches no mapping is *silently not drawn*. Nothing here moves by state bits, so facing stays out of the word entirely. - **`state = AKGL_ACTOR_STATE_ALIVE`** — the word the character mapping names. - **`visible = true`.** `akgl_actor_initialize()` does not raise it. In a tilemap game the map loader copies visibility from map data; there is no map here, so an actor that skips this line exists, moves, fires and collides — invisibly. This one line cost this example its first screenshot. Input goes through a control map: push a control per key with handlers that set flags, and let the actor's update hook read the flags. The full recipe is in `examples/galaga/player.c` and libakgl docs/16-input.md; the shape is: ```c wrap=galagagame requires=akgl static akerr_ErrorContext *galaga_player_controls(void) { akgl_Control control; PREPARE_ERROR(errctx); memset(&control, 0, sizeof(control)); control.event_on = SDL_EVENT_KEY_DOWN; control.event_off = SDL_EVENT_KEY_UP; control.key = SDLK_LEFT; control.handler_on = &left_on; control.handler_off = &left_off; PASS(errctx, akgl_controller_pushmap(0, &control)); control.key = SDLK_RIGHT; control.handler_on = &right_on; control.handler_off = &right_off; PASS(errctx, akgl_controller_pushmap(0, &control)); control.key = SDLK_SPACE; control.handler_on = &fire_on; control.handler_off = &fire_off; PASS(errctx, akgl_controller_pushmap(0, &control)); akgl_controlmaps[0].target = galaga_game.player; SUCCEED_RETURN(errctx); } ``` Hand **every** polled event to `akgl_controller_handle_event()` — one that no control binds is not an error, it is a call that did nothing. ## Step 4: Shots and collision **Goal: bullets that fly, hit, and give their actor slot back.** Bullets and collision are C forever — they are engine, not behavior. A shot is an actor from the same 64-slot heap pool, with its own tiny update hook: move, test, release. ```c wrap=galagagame requires=akgl static akerr_ErrorContext *player_shot_update(akgl_Actor *obj) { SDL_FRect mine; SDL_FRect theirs; bool hit = false; int i = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); obj->y -= 900.0f * galaga_game.dt; if ( obj->y < -60.0f ) { galaga_game.player_shots_live -= 1; PASS(errctx, akgl_heap_release_actor(obj)); SUCCEED_RETURN(errctx); } shot_box(obj, &mine); for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) { if ( galaga_enemy_actors[i] == NULL ) { continue; } enemy_box(galaga_enemy_actors[i], &theirs); PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit)); if ( !hit ) { continue; } galaga_enemies[i].hp -= 1; if ( galaga_enemies[i].hp <= 0 ) { PASS(errctx, kill_enemy(i)); } galaga_game.player_shots_live -= 1; PASS(errctx, akgl_heap_release_actor(obj)); SUCCEED_RETURN(errctx); } SUCCEED_RETURN(errctx); } ``` Four conventions worth keeping: - **`akgl_collide_rectangles()` is the whole collision system.** At most 2 shots x 40 enemies of axis-aligned tests per frame is noise; the full `akgl_CollisionWorld` machinery earns its keep on tilemaps, not here. The `shot_box`/`enemy_box` helpers inset each box from the artwork's rectangle, because the PNGs carry transparent margin that should not kill anybody. - **Releasing is despawning.** `akgl_heap_release_actor()` unregisters the actor and stops it drawing; releasing mid-sweep is safe because `akgl_game_update()` re-reads each slot's refcount as it goes. - **Names carry a serial** — `pshot17`, not `pshot1` reused — because the actor registry is keyed by name, and two live actors with one name is a fight. - **Spawn caps are C-side refusals.** Two player shots, eight enemy shots; the spawn functions simply decline past the cap. Give the enemy shots the same shape falling downward, and the ship a sweep over both — `examples/galaga/player.c` has all three loops. ## Step 5: Boot the interpreter **Goal: the engine calls a BASIC function and prints its answer.** Everything so far was libakgl. Now link the interpreter into the same executable. In CMake: ```cmake target_link_libraries(akbasic_example_galaga PRIVATE akbasic akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image) ``` Link `akbasic` — the interpreter only. Not `akbasic_akgl` (the device backends that let a script draw), and not `akbasic_frontend` (the standalone program's host). This game lends the script **no devices at all**: the scripts compute, the engine draws, and a script that tries `SPRITE` is refused by name. That refusal is enforced by the interpreter, not by convention — [Chapter 10](10-embedding.md) explains the device-lending model this game declines to use. The boot is the embedding host from Chapter 10, adapted to a script that only defines. Keep every line that touches the interpreter in one file — the example's `script.c` — so the boundary stays a place rather than a habit: ```c wrap=galagacalls requires=akgl CATCH(errctx, akbasic_error_register()); CATCH(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL)); CATCH(errctx, akbasic_runtime_init(&SCRIPT, &SINK)); CATCH(errctx, akbasic_runtime_load(&SCRIPT, SOURCE)); CATCH(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN)); CATCH(errctx, akbasic_runtime_run(&SCRIPT, 4 * AKBASIC_MAX_SOURCE_LINES)); CATCH(errctx, akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN)); ``` Two of those lines are the ones a first embedding gets wrong. **A "no top level code" script still has to run once.** The script is nothing but `DEF` blocks and a final `END`, and executing the `DEF` statements is what files the functions. The run is bounded — a script that is all definitions has no business taking more than a few steps per line, and an accidental loop at boot should be a diagnosis, not a hang. **The `set_mode` after the run is load-bearing.** The program has now ended and the runtime sits in QUIT mode, where a multi-line `DEF` called from the host returns a silent zero. Forcing the mode back to RUN makes the bodies run, and it stays put because nothing here ever steps the runtime again. Issue #8 tracks making this workaround unnecessary. `PRINT` inside the script goes through the stdio sink and lands on stdout — that is the script's debug channel for the rest of both chapters. Prove the wiring with one function. Put this in the script: ```basic DEF ADDEM(A#, B#) = A# + B# END ``` And call it from C, with values you already have: ```c wrap=galagacalls requires=akgl memset(&args[0], 0, sizeof(args[0])); memset(&args[1], 0, sizeof(args[1])); args[0].valuetype = AKBASIC_TYPE_INTEGER; args[0].intval = 17; args[1].valuetype = AKBASIC_TYPE_INTEGER; args[1].intval = 25; argp[0] = &args[0]; argp[1] = &args[1]; CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, "ADDEM", argp, 2, &result)); printf("ADDEM(17, 25) = %lld\n", (long long)result->intval); ``` ```text ADDEM(17, 25) = 42 ``` `akbasic_runtime_call_function()` is the host's entry point: a name and already-evaluated values in, the function's result out. The engine refuses to start when the script will not boot — a game whose enemies cannot think is not a game missing a feature, it is a game that does not run. ## Step 6: The update hook **Goal: one actor whose every frame is a BASIC call.** `akgl_game_update()` calls each live actor's `updatefunc` exactly once per frame. Replacing that pointer is the whole integration: the actor's frame *is* a script call. ```c wrap=galagagame requires=akgl static akerr_ErrorContext *enemy_update(akgl_Actor *obj) { galaga_Enemy *enemy = NULL; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); enemy = (galaga_Enemy *)obj->actorData; FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "an enemy actor with no galaga_Enemy attached"); enemy->rnd = galaga_random(); PASS(errctx, galaga_script_update_enemy(enemy, obj, galaga_game.dt)); if ( enemy->fire != 0 ) { PASS(errctx, enemy_fire(enemy, obj)); } SUCCEED_RETURN(errctx); } ``` The hook's body is a protocol, and `galaga_script_update_enemy()` is its middle: **rebind, call, recover, reset.** ```c wrap=galagacalls requires=akgl CATCH(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", enemy)); CATCH(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", actor)); memset(&dtval, 0, sizeof(dtval)); dtval.valuetype = AKBASIC_TYPE_FLOAT; dtval.floatval = (double)dt; argp[0] = &dtval; CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, "UPDATEBEE", argp, 1, &result)); CATCH(errctx, akbasic_environment_zero(SCRIPT.environment)); ``` `SELF@` and `ACTOR@` are **host bindings** — the enemy's record and the engine's live actor, shared with the script as structures it can read and write directly. [Chapter 21](21-tutorial-galaga-enemies.md) builds them; for this chapter, know that `akbasic_host_rebind()` points an existing binding at a different instance, which is how forty enemies share one script: one name, rebound per enemy, rather than forty names. **The `akbasic_environment_zero()` after every call is load-bearing.** Each call parks its result in the caller environment's per-line value scratch, and a host calling in a loop never crosses the line boundary that would reset it. Without this line the scratch drains in under two frames of a 40-enemy wave and every later call fails with `Maximum values per line reached`. Chapter 10's ["Calling a function every frame"](10-embedding.md#calling-a-function-every-frame) section is the rule's home. ## Step 7: First light **Goal: a C actor moving under BASIC control, and proof it is one memory.** Before any real AI, the smallest demonstration. One enemy, one function, a sine drift written entirely in BASIC through the actor binding: ```basic DEF UPDATEBEE(DT%) SELF@.T% = SELF@.T% + DT% ACTOR@.X% = 590.0 + SIN(SELF@.T%) * 200 ACTOR@.Y% = 300.0 PRINT "BASIC SEES X = " + ACTOR@.X% RETURN 0 END ``` Spawn one enemy with the hook from Step 6, and have the engine print the same actor's position each frame from C: ```c wrap=galagahost requires=akgl SDL_Log("C SEES X = %f", galaga_enemy_actors[0]->x); ``` ```text BASIC SEES X = 593.191094 INFO: C SEES X = 593.191094 BASIC SEES X = 596.378593 INFO: C SEES X = 596.378593 ``` Same numbers, one memory. The script wrote `ACTOR@.X%`; the renderer read `akgl_Actor.x`; nothing copied anything anywhere. The ship swings in a slow arc, and the whole architecture is visible in that one motion: C owns the frame, BASIC owns the decision, and the actor is the same bytes to both. ## Step 8: Screens **Goal: title, playing, game over, victory — a state machine around the loop.** The screens are libakgl's UI layer, in the three-state pattern of its uidemo example (libakgl docs/22-ui.md). A `galaga_Screen` enum, one `declare_*()` function per screen, and the UI bracket between `akgl_game_update()` and `frame_end` — exactly where the frame contract puts it: ```c wrap=galagahost requires=akgl CATCH(errctx, akgl_ui_frame_begin()); switch ( galaga_game.screen ) { case GALAGA_SCREEN_TITLE: CATCH(errctx, declare_title()); break; case GALAGA_SCREEN_PLAY: CATCH(errctx, declare_play()); break; case GALAGA_SCREEN_GAMEOVER: case GALAGA_SCREEN_VICTORY: CATCH(errctx, declare_end()); break; } CATCH(errctx, akgl_ui_frame_end(akgl_renderer)); ``` The playing screen is two `akgl_ui_label()` calls — score top-left, lives and wave top-right — formatted into `static` buffers, because the UI borrows label text until `frame_end` and a local buffer would be dangling by the time it draws. The title and end screens are an `akgl_ui_menu()` at the center. The big **GALAGA** headline is direct text rather than a label: ```c wrap=galagagame requires=akgl static akerr_ErrorContext *draw_banner(char *text) { SDL_Color ink = { 235, 235, 235, 255 }; TTF_Font *font = NULL; int w = 0; int h = 0; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "text"); font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "banner", NULL); FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "the banner font is not loaded"); PASS(errctx, akgl_text_measure(font, text, &w, &h)); PASS(errctx, akgl_text_rendertextat(font, text, ink, 0, (1280 - w) / 2, 280)); SUCCEED_RETURN(errctx); } ``` The menu owns `AKGL_UI_ANCHOR_CENTER`, a label anchored there disappears behind it, and there is no top-center anchor — so the headline measures itself and draws at a coordinate, before the UI bracket so the menu still paints over it if the two ever meet. ![The title screen: the banner, the menu, the starfield](images/galaga-title.png) Screen transitions are three rules read after the world updates: lives spent is GAME OVER, an empty wave is VICTORY, and a menu activation either restarts or quits. The menu never clears its own `activated` flag — the state machine that acts on it does. ## Step 9: Run it headless **Goal: the same game, playable by a script, in CI every night.** The example takes five flags, in the pattern of libakgl's sidescroller: ```sh norun $ SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy SDL_RENDER_DRIVER=software \ ./build-akgl/akbasic_example_galaga --frames 600 --autoplay ``` `--frames N` bounds the run; `--autoplay` is a scripted pilot that starts the game, sweeps the floor and holds fire until the wave assembles; `--screenshot PATH --screenshot-frame N` write a PNG from the render target — the figures in this chapter are that flag's output, not pictures somebody took once. Synthetic input goes through `akgl_controller_handle_event()` with constructed `SDL_Event`s, never by calling the handlers directly — the point of autoplay is to exercise the same path a keyboard does. The last line of every run is the evidence: ```text galaga: 600 frames, screen 1, score 1910, alive 9, kills bee 19 bfly 12 boss 0, shots bee 0 bfly 3 boss 0, script errors 0 ``` Exiting 0 is not proof the wave flew. The readout is: kills and shots counted per kind say the enemies entered, thought and fired, and **`script errors 0`** says every one of the ~24,000 BASIC calls in those ten seconds came back clean. A wave of dumb enemies still exits 0, and that count is how you notice. The CTest entry `example_galaga` runs exactly this under the dummy SDL drivers, which is what keeps both chapters honest. --- That is the engine: a window, a starfield, a ship, bullets, screens, and an interpreter that answers when called. Everything on screen so far is C. What turns it into a GALAGA is [Chapter 21](21-tutorial-galaga-enemies.md) — the three shared structures, the script that thinks through them, and a full wave entering, breathing, diving and firing without another line of engine code.