A GALAGA tutorial: C/libakgl engine with akbasic embedded as the enemy-behavior engine #37

Open
tachikoma wants to merge 14 commits from galaga-tutorial into main
4 changed files with 155 additions and 6 deletions
Showing only changes of commit dd10dc143a - Show all commits

View File

@@ -125,7 +125,38 @@ 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 **Error handling is the house protocol.** Every function returns
`akerr_ErrorContext *`, `PASS` propagates, `ATTEMPT`/`CATCH`/`CLEANUP` brackets `akerr_ErrorContext *`, `PASS` propagates, `ATTEMPT`/`CATCH`/`CLEANUP` brackets
anything that must unwind. libakgl's docs/04-errors.md teaches it; this chapter anything that must unwind. libakgl's docs/04-errors.md teaches it; this chapter
just uses it. The status codes this game raises are `AKERR_NULLPOINTER`, just uses it, with two rules that keep the fragments compiling: **`CATCH` is
only legal inside an `ATTEMPT` block, and `PASS` everywhere else** — swap them
and the compiler objects about a stray `break` — and `main()` alone ends its
block with `FINISH_NORETURN(errctx)` instead of `FINISH`, because `FINISH`
expands a `return` of the context that an `int`-returning function cannot
compile:
```c wrap=galagatypes requires=akgl
static int FAILED = 0;
int main(int argc, char *argv[])
{
PREPARE_ERROR(errctx);
(void)argc; (void)argv;
ATTEMPT {
/* CATCH each stage in order: startup, assets, the script boot,
* the spawns, then the frame loop. */
} CLEANUP {
/* ...teardown, every call wrapped in IGNORE()... */
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "galaga could not run");
/* Set a flag rather than returning: leaving a HANDLE block early
* skips FINISH's release and leaks the context's pool slot. */
FAILED = 1;
} FINISH_NORETURN(errctx);
return FAILED;
}
```
The status codes this game raises are `AKERR_NULLPOINTER`,
`AKERR_VALUE`, `AKERR_KEY`, `AKERR_IO`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL` `AKERR_VALUE`, `AKERR_KEY`, `AKERR_IO`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL`
and `AKGL_ERR_HEAP` — there is no code this tutorial invents. and `AKGL_ERR_HEAP` — there is no code this tutorial invents.
@@ -285,8 +316,32 @@ Each of the four lines under the comment closes a trap:
invisibly. This one line cost this example its first screenshot. 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 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 flags, and let the actor's update hook read the flags. A handler receives the
`examples/galaga/player.c` and libakgl docs/16-input.md; the shape is: map's target actor and the event, and returns through the error protocol like
everything else — this pair is the whole pattern, repeated per key:
```c wrap=galagagame requires=akgl
static bool MOVELEFT = false;
akerr_ErrorContext *left_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
MOVELEFT = true;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *left_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
MOVELEFT = false;
SUCCEED_RETURN(errctx);
}
```
(The example keeps the flags in its `galaga_Game` struct rather than statics;
either works.) The bindings themselves are pushes onto map 0:
```c wrap=galagagame requires=akgl ```c wrap=galagagame requires=akgl
static akerr_ErrorContext *galaga_player_controls(void) static akerr_ErrorContext *galaga_player_controls(void)
@@ -673,10 +728,53 @@ case GALAGA_SCREEN_VICTORY:
CATCH(errctx, akgl_ui_frame_end(akgl_renderer)); CATCH(errctx, akgl_ui_frame_end(akgl_renderer));
``` ```
The playing screen is two `akgl_ui_label()` calls — score top-left, lives and The playing screen is two `akgl_ui_label()` calls — a widget call per label,
wave top-right — formatted into `static` buffers, because the UI borrows label not a struct — formatted into `static` buffers, because the UI borrows label
text until `frame_end` and a local buffer would be dangling by the time it 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. draws:
```c wrap=galagagame requires=akgl
static char HUD_SCORE[64];
static char HUD_LIVES[64];
static akerr_ErrorContext *declare_play(void)
{
int count = 0;
PREPARE_ERROR(errctx);
PASS(errctx, aksl_snprintf(&count, HUD_SCORE, sizeof(HUD_SCORE),
"SCORE %06d", galaga_game.score));
PASS(errctx, aksl_snprintf(&count, HUD_LIVES, sizeof(HUD_LIVES),
"LIVES %d WAVE %d", galaga_game.lives, galaga_shared.wave));
PASS(errctx, akgl_ui_label("score", HUD_SCORE, AKGL_UI_ANCHOR_TOP_LEFT, NULL));
PASS(errctx, akgl_ui_label("lives", HUD_LIVES, AKGL_UI_ANCHOR_TOP_RIGHT, NULL));
SUCCEED_RETURN(errctx);
}
```
The title and end screens are an `akgl_ui_menu()` at the center, fed an
`akgl_UiMenu` that lives in a `static` for the same borrowing reason. The
struct is an id, the item strings, a count, the selected index, the
`activated` output flag, and a style (`NULL` for the default):
```c wrap=galagatypes requires=akgl
static akgl_UiMenu TITLE_MENU = {
"titlemenu", { "START", "QUIT" }, 2, 0, false, NULL
};
static akerr_ErrorContext *declare_title(void)
{
PREPARE_ERROR(errctx);
PASS(errctx, akgl_ui_menu(&TITLE_MENU));
SUCCEED_RETURN(errctx);
}
```
Route events to the menu with
`akgl_ui_menu_handle_event(&TITLE_MENU, event, &consumed)` — the menu for
whichever screen is up, a `bool` out-parameter reporting whether the event was
taken. Up and down move `selected`, return sets `activated`.
The big **GALAGA** headline is direct text rather than a label: The big **GALAGA** headline is direct text rather than a label:

View File

@@ -157,6 +157,24 @@ between the script's decision and the engine's pixel. Null physics
(Chapter 20, Step 1) is what makes that safe: nothing else is trying to move (Chapter 20, Step 1) is what makes that safe: nothing else is trying to move
the actor. the actor.
Registration and the first binding happen at boot, before the script loads —
between `akbasic_runtime_init()` and `akbasic_runtime_load()` in Chapter 20's
boot sequence. A binding is **borrowed, never copied**, so the placeholders it
points at must be static storage:
```c wrap=galagacalls requires=akgl
CATCH(errctx, akbasic_host_register_type(&SCRIPT, &ENEMY_TYPE));
CATCH(errctx, akbasic_host_register_type(&SCRIPT, &ACTOR_TYPE));
CATCH(errctx, akbasic_host_register_type(&SCRIPT, &GAME_TYPE));
CATCH(errctx, akbasic_host_bind(&SCRIPT, "SELF@", "ENEMY", &SCRATCH_ENEMY));
CATCH(errctx, akbasic_host_bind(&SCRIPT, "ACTOR@", "ACTOR", &SCRATCH_ACTOR));
CATCH(errctx, akbasic_host_bind(&SCRIPT, "GAME@", "GAME", &galaga_shared));
```
`akbasic_host_bind()` takes the script name, the registered type's name, and
the instance; after that, `SELF@` and `ACTOR@` are only ever *re*bound.
The per-frame call binds both names to *this* enemy before dispatching — one The per-frame call binds both names to *this* enemy before dispatching — one
binding per name, pointed at forty enemies in turn, which is what binding per name, pointed at forty enemies in turn, which is what
`akbasic_host_rebind()` is for: `akbasic_host_rebind()` is for:

View File

@@ -4,5 +4,7 @@
(void)SCRIPT; (void)SINK; (void)SINKSTATE; (void)SOURCE; (void)SCRIPT; (void)SINK; (void)SINKSTATE; (void)SOURCE;
(void)args; (void)argp; (void)dtval; (void)result; (void)args; (void)argp; (void)dtval; (void)result;
(void)enemy; (void)actor; (void)dt; (void)enemy; (void)actor; (void)dt;
(void)SCRATCH_ENEMY; (void)SCRATCH_ACTOR; (void)galaga_shared;
(void)ENEMY_TYPE; (void)ACTOR_TYPE; (void)GAME_TYPE;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -36,11 +36,42 @@ typedef struct galaga_docs_Enemy
float rnd; float rnd;
} galaga_docs_Enemy; } galaga_docs_Enemy;
typedef struct galaga_docs_Shared
{
float playerx;
float playery;
int32_t wave;
float rnd;
} galaga_docs_Shared;
static akbasic_Runtime SCRIPT; static akbasic_Runtime SCRIPT;
static akbasic_TextSink SINK; static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE; static akbasic_StdioSink SINKSTATE;
static char SOURCE[16384]; static char SOURCE[16384];
static galaga_docs_Enemy SCRATCH_ENEMY;
static akgl_Actor SCRATCH_ACTOR;
static galaga_docs_Shared galaga_shared;
static const akbasic_HostField ENEMY_FIELDS[] = {
AKBASIC_HOST_FIELD( galaga_docs_Enemy, kind, "KIND#", AKBASIC_HOSTFIELD_INT32 )
};
static const akbasic_HostType ENEMY_TYPE = {
"ENEMY", sizeof(galaga_docs_Enemy), ENEMY_FIELDS, 1
};
static const akbasic_HostField ACTOR_FIELDS[] = {
AKBASIC_HOST_FIELD( akgl_Actor, x, "X%", AKBASIC_HOSTFIELD_FLOAT )
};
static const akbasic_HostType ACTOR_TYPE = {
"ACTOR", sizeof(akgl_Actor), ACTOR_FIELDS, 1
};
static const akbasic_HostField GAME_FIELDS[] = {
AKBASIC_HOST_FIELD( galaga_docs_Shared, wave, "WAVE#", AKBASIC_HOSTFIELD_INT32 )
};
static const akbasic_HostType GAME_TYPE = {
"GAME", sizeof(galaga_docs_Shared), GAME_FIELDS, 1
};
akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(galaga_docs_Enemy *enemy, akgl_Actor *actor, float dt); akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(galaga_docs_Enemy *enemy, akgl_Actor *actor, float dt);
akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(galaga_docs_Enemy *enemy, akgl_Actor *actor, float dt) akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(galaga_docs_Enemy *enemy, akgl_Actor *actor, float dt)
{ {