A GALAGA tutorial: C/libakgl engine with akbasic embedded as the enemy-behavior engine #37
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user