850 lines
30 KiB
Markdown
850 lines
30 KiB
Markdown
|
|
# 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:
|
||
|
|
|
||
|
|

|
||
|
|
|
||
|
|
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;
|
||
|
|