Deliver GALAGA tutorial and akbasic enemy scripting example #39

Closed
logikoma wants to merge 7 commits from 34 into feature/reduce_memory_usage
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
`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 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`
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.
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:
flags, and let the actor's update hook read the flags. A handler receives the
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
static akerr_ErrorContext *galaga_player_controls(void)
@@ -673,10 +728,53 @@ case GALAGA_SCREEN_VICTORY:
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
The playing screen is two `akgl_ui_label()` calls — a widget call per 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
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:

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
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
binding per name, pointed at forty enemies in turn, which is what
`akbasic_host_rebind()` is for:

View File

@@ -4,5 +4,7 @@
(void)SCRIPT; (void)SINK; (void)SINKSTATE; (void)SOURCE;
(void)args; (void)argp; (void)dtval; (void)result;
(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);
}

View File

@@ -36,11 +36,42 @@ typedef struct galaga_docs_Enemy
float rnd;
} 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_TextSink SINK;
static akbasic_StdioSink SINKSTATE;
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)
{