akgl_rectangle_points, akgl_collide_point_rectangle, akgl_Point and akgl_RectanglePoints go. They were the intermediate form of an implementation that changed: akgl_collide_rectangles was eight corner-containment tests built on them, and it has been four span comparisons since the cross-case fix. Nothing outside tests/ called either function, and a point-in-rectangle test is four comparisons a caller can write without a struct conversion in front of them. akgl_collide_rectangles stays. It has two correct callers in the sidescroller asking a game-level overlap question -- a coin, a hazard, from an updatefunc -- where a bool is the whole answer and a proxy plus a narrowphase call would be computing a normal nothing reads. TODO.md records the split rather than leaving it to be rediscovered. Public API removal, so 194 exported akgl_ symbols against 196, and the manual's counts move with them. The perf suite loses its rectangle_points row; the all-pairs sweep stays as the control it is now labelled, and PERFORMANCE.md says what 0.8.0 measured against it -- 188.5 us for 32,640 pairs at 256 actors, where a whole step with collision attached is 54.1 us doing strictly more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
299 lines
13 KiB
Markdown
299 lines
13 KiB
Markdown
# 02. Design philosophy
|
|
|
|
Six decisions shape every API in this library. They are not preferences stated up front and
|
|
then quietly abandoned in the implementation; each one is visible in the code, and this
|
|
chapter shows the code that proves it.
|
|
|
|
None of this is an argument that you should build a game this way. It is an explanation of
|
|
what libakgl will and will not do for you, so you can decide whether the tradeoffs are ones
|
|
you want.
|
|
|
|
## Bounded, pre-declared resources — not dynamic ones
|
|
|
|
**libakgl does not call `malloc` at runtime.** Every runtime object comes out of a fixed,
|
|
statically allocated array declared in `heap.h`. There are five such arrays — actors,
|
|
sprites, spritesheets, characters and strings — and the ceilings are compile-time constants:
|
|
|
|
```c excerpt=include/akgl/heap.h
|
|
#ifndef AKGL_MAX_HEAP_ACTOR
|
|
#define AKGL_MAX_HEAP_ACTOR 64
|
|
#endif
|
|
#ifndef AKGL_MAX_HEAP_SPRITE
|
|
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
|
|
#endif
|
|
#ifndef AKGL_MAX_HEAP_SPRITESHEET
|
|
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
|
|
#endif
|
|
#ifndef AKGL_MAX_HEAP_CHARACTER
|
|
#define AKGL_MAX_HEAP_CHARACTER 256
|
|
#endif
|
|
#ifndef AKGL_MAX_HEAP_STRING
|
|
#define AKGL_MAX_HEAP_STRING 256
|
|
#endif
|
|
```
|
|
|
|
Allocation is a linear scan for a slot whose reference count is zero. That is the whole
|
|
algorithm:
|
|
|
|
```c excerpt=src/heap.c
|
|
akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
for (int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
|
if ( akgl_heap_actors[i].refcount != 0 ) {
|
|
continue;
|
|
}
|
|
*dest = &akgl_heap_actors[i];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
|
|
}
|
|
```
|
|
|
|
Two consequences follow, and both matter more than the absence of `malloc`.
|
|
|
|
**Exhaustion is a normal error, not a catastrophe.** `AKGL_ERR_HEAP` means "this pool is
|
|
full". It is not `ENOMEM`, the process is not in trouble, and the fix is almost never a
|
|
bigger pool — **it is usually a missing release.** Raising `AKGL_MAX_HEAP_*` before you have
|
|
checked for a leak converts a bug that fails on frame 300 into one that fails on frame 3000.
|
|
|
|
**The pools are compile-time sized, so the ceiling is an ABI constraint.** The arrays live
|
|
in the library, so libakgl and everything linking it must agree on the numbers. Overriding
|
|
one means rebuilding the whole tree, not just your game.
|
|
|
|
The idiom for a pool object is claim, use, release in `CLEANUP`, which runs on every path
|
|
out of the `ATTEMPT` block:
|
|
|
|
```c
|
|
#include <akerror.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/staticstring.h>
|
|
#include <akgl/util.h>
|
|
|
|
/* Claim, use, release. The CLEANUP block runs on every path out of ATTEMPT. */
|
|
akerr_ErrorContext *scratch_path(char *root, char *name)
|
|
{
|
|
akgl_String *buf = NULL;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
PASS(errctx, akgl_heap_next_string(&buf));
|
|
|
|
ATTEMPT {
|
|
CATCH(errctx, akgl_string_initialize(buf, name));
|
|
CATCH(errctx, akgl_path_relative(root, buf->data, buf));
|
|
SDL_Log("resolved to %s", buf->data);
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_string(buf));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
```
|
|
|
|
**If the pool has no layer for your type, add one.** A new kind of runtime object gets a new
|
|
array, a `next`, and a `release`, in `heap.h` and `src/heap.c`. It does not get an
|
|
allocator. [Chapter 5](05-the-heap.md) covers the layers, the reference-count asymmetry
|
|
between `akgl_heap_next_string` and the other seven, and what the ceilings cost you.
|
|
|
|
## Variation lives in a backend, not in a branch
|
|
|
|
There are exactly three pluggable subsystems, and all have the same shape: **a struct of
|
|
function pointers, plus an initializer that populates it.**
|
|
|
|
```text
|
|
akgl_RenderBackend akgl_PhysicsBackend akgl_Partitioner
|
|
+--------------------+ +--------------------+ +-----------------+
|
|
| sdl_renderer | | simulate | | reset |
|
|
| shutdown | | gravity | | insert |
|
|
| frame_start | | collide | | remove |
|
|
| frame_end | | move | | move |
|
|
| draw_texture | | drag_x/y/z | | query |
|
|
| draw_mesh | | gravity_x/y/z | | each_pair |
|
|
| draw_world | | max_timestep | | state |
|
|
+--------------------+ | collision | +-----------------+
|
|
^ +--------------------+ ^
|
|
| populated by ^ | populated by
|
|
akgl_render_2d_bind | populated by akgl_partitioner_init_grid
|
|
akgl_render_2d_init akgl_physics_init_null akgl_partitioner_init_bsp
|
|
akgl_physics_init_arcade (akgl_partitioner_factory)
|
|
(akgl_physics_factory)
|
|
```
|
|
|
|
The shipped physics initializers are five assignments and nothing else:
|
|
|
|
```c excerpt=src/physics.c
|
|
akerr_ErrorContext *akgl_physics_init_null(akgl_PhysicsBackend *self)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
self->gravity = akgl_physics_null_gravity;
|
|
self->collide = akgl_physics_null_collide;
|
|
self->move = akgl_physics_null_move;
|
|
self->simulate = akgl_physics_simulate;
|
|
```
|
|
|
|
**To vary the behaviour, write an initializer and register it — do not add a conditional.**
|
|
Nothing in `akgl_physics_simulate` knows which backend it is stepping, and nothing in the
|
|
render path knows whether `draw_texture` came from the 2D backend or from yours. You can
|
|
also mix: borrow the entry points that already do what you want and replace only the one
|
|
that does not.
|
|
|
|
```c
|
|
#include <akerror.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/physics.h>
|
|
#include <akgl/actor.h>
|
|
|
|
/* One tick of "everything floats up at a constant rate", as a backend. */
|
|
static akerr_ErrorContext *floaty_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
|
actor->ey = -40.0f * dt;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *floaty_init(akgl_PhysicsBackend *self)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
|
|
|
/* Borrow the parts that already do the right thing. */
|
|
self->simulate = akgl_physics_simulate;
|
|
self->move = akgl_physics_arcade_move;
|
|
self->collide = akgl_physics_null_collide;
|
|
self->gravity = floaty_gravity;
|
|
|
|
self->gravity_time = SDL_GetTicksNS();
|
|
self->max_timestep = AKGL_PHYSICS_DEFAULT_MAX_TIMESTEP;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
```
|
|
|
|
There is no inheritance, no RTTI and no dispatch on a type tag anywhere in the library.
|
|
Behaviour attaches as a function pointer on a struct — which is also how an actor's own six
|
|
behaviour hooks work, in [Chapter 12](12-actors.md).
|
|
|
|
## Bit flags are the default representation for state
|
|
|
|
Where another library would give you an enum and a setter per property, libakgl gives you a
|
|
32-bit mask and four macros. An actor's state is the clearest case: it is a *set*, not a
|
|
value, because an actor is alive and facing left and moving left all at once.
|
|
|
|
```c excerpt=include/akgl/actor.h
|
|
#define AKGL_ACTOR_STATE_FACE_DOWN (1 << 0) // 1 0000 0000 0000 0001
|
|
#define AKGL_ACTOR_STATE_FACE_LEFT (1 << 1) // 2 0000 0000 0000 0010
|
|
#define AKGL_ACTOR_STATE_FACE_RIGHT (1 << 2) // 4 0000 0000 0000 0100
|
|
#define AKGL_ACTOR_STATE_FACE_UP (1 << 3) // 8 0000 0000 0000 1000
|
|
#define AKGL_ACTOR_STATE_ALIVE (1 << 4) // 16 0000 0000 0001 0000
|
|
```
|
|
|
|
The iterator flags in `iterator.h` are the same idea for "what should this sweep do":
|
|
|
|
```c excerpt=include/akgl/iterator.h
|
|
#define AKGL_ITERATOR_OP_UPDATE (1 << 0) // 1 Call the actor's updatefunc
|
|
#define AKGL_ITERATOR_OP_RENDER (1 << 1) // 2 Call the actor's renderfunc
|
|
#define AKGL_ITERATOR_OP_RELEASE (1 << 2) // 4 Release the object back to its heap layer
|
|
#define AKGL_ITERATOR_OP_LAYERMASK (1 << 3) // 8 Skip anything whose layer != layerid
|
|
#define AKGL_ITERATOR_OP_TILEMAPSCALE (1 << 4) // 16 Scale actors to the tilemap; otherwise force scale 1.0
|
|
```
|
|
|
|
Both tables are hand-aligned, with the decimal value and the bit pattern in a comment per
|
|
row, and both are protected from the reindent script's `tabify` step precisely so that
|
|
alignment survives. The tables in the headers are the reference; these excerpts are checked
|
|
against them.
|
|
|
|
**Use the macros, not the operators.** `AKGL_BITMASK_HAS(x, y)` is "every bit of `y` is set
|
|
in `x`", not "any of them". It is fully parenthesized so that `!AKGL_BITMASK_HAS(a, b)`
|
|
means what it reads as — until 0.5.0 it did not, and the negation bound to the `&`. Nothing
|
|
in the tree negated it, which is the only reason that was latent rather than live.
|
|
|
|
## Errors carry context, and callers cannot ignore them
|
|
|
|
**181 of libakgl's 193 functions return `akerr_ErrorContext AKERR_NOIGNORE *`.**
|
|
Results come back through pointer parameters; the return value is always the error. The
|
|
`AKERR_NOIGNORE` attribute makes discarding it a compiler diagnostic.
|
|
|
|
That is a libakerror design and it is documented by libakerror.
|
|
[Chapter 4](04-errors.md) covers the part that is genuinely libakgl's: which statuses these
|
|
functions raise, what each one means *here*, and the traps libakgl's own structure creates.
|
|
Read it before any subsystem chapter.
|
|
|
|
The reasoning behind the choice is operational rather than aesthetic. A game that fails on
|
|
somebody else's machine leaves you a log file and nothing else, so an error has to say what
|
|
failed, where, and why, at the point it happened — not three frames later at a `NULL`
|
|
dereference with no history attached.
|
|
|
|
## Everything is referenced by name
|
|
|
|
Nothing in this library is passed around by pointer where a name will do. An actor names the
|
|
character it instantiates; a character names the sprites it draws for each state; a sprite
|
|
names the sheet it cuts frames from. All of it is resolved at load time through eight
|
|
registries:
|
|
|
|
```c excerpt=include/akgl/registry.h
|
|
extern SDL_PropertiesID AKGL_REGISTRY_ACTOR;
|
|
extern SDL_PropertiesID AKGL_REGISTRY_SPRITE;
|
|
extern SDL_PropertiesID AKGL_REGISTRY_SPRITESHEET;
|
|
extern SDL_PropertiesID AKGL_REGISTRY_CHARACTER;
|
|
```
|
|
|
|
The lookup itself is unremarkable, and that is the point:
|
|
|
|
```c excerpt=src/actor.c
|
|
obj->basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, basecharname, NULL);
|
|
```
|
|
|
|
**This is what lets the whole asset graph be described in JSON files that reference each
|
|
other by name**, with no build step and no code generation. It also means a typo in an asset
|
|
file is a runtime failure rather than a compile error — which is the tradeoff, stated
|
|
plainly. The registries are SDL property sets rather than an akgl type, so you can enumerate
|
|
one with `SDL_EnumerateProperties`; that is exactly what `akgl_registry_iterate_actor` does.
|
|
|
|
Two sharp edges, both in [Chapter 6](06-the-registry.md): an uninitialized registry id is 0,
|
|
which SDL treats as "no such property set" and silently drops writes to; and
|
|
`AKGL_REGISTRY_SPRITESHEET` is keyed on the *resolved* image path, which is what makes two
|
|
sprites naming the same PNG share one texture.
|
|
|
|
## One world at a time
|
|
|
|
The library keeps exactly one of everything, reachable through four globals:
|
|
|
|
```c excerpt=include/akgl/game.h
|
|
extern akgl_Tilemap *akgl_gamemap;
|
|
extern akgl_RenderBackend *akgl_renderer;
|
|
extern akgl_PhysicsBackend *akgl_physics;
|
|
extern SDL_FRect *akgl_camera;
|
|
```
|
|
|
|
`akgl_game_init` points each of them at default storage — `akgl_default_renderer`,
|
|
`akgl_default_physics`, `akgl_default_camera`, `akgl_default_gamemap`. **They are pointers
|
|
so that you can substitute your own instance by reassigning one**, and the rest of the
|
|
library goes on drawing and simulating through it without knowing.
|
|
|
|
That is the entire extent of the indirection. **There is no notion of two worlds at once.**
|
|
No context handle is threaded through the API, no `akgl_World *` is passed to every call,
|
|
and two simultaneous games in one process is not a thing this library supports. If you want
|
|
a paused world behind a menu, you save the state you care about and swap the pointers back.
|
|
|
|
The globals all carry the `akgl_` prefix, and that is not cosmetic. Until 0.5.0 the library
|
|
exported a bare `renderer`, `tests/character.c` defined an `SDL_Renderer *renderer` of its
|
|
own, both had external linkage with the same spelling, and the executable's definition
|
|
preempted the library's. Every texture load in that suite failed and the suite reported
|
|
success anyway. A consuming game with a variable called `renderer` would have hit exactly
|
|
the same thing, with no test to notice.
|
|
|
|
## What the six add up to
|
|
|
|
You get a library whose worst-case memory is knowable at compile time, whose failure modes
|
|
arrive as messages instead of crashes, whose behaviour you extend by writing a function
|
|
rather than by editing a switch, and whose assets are text files you can edit by hand.
|
|
|
|
You give up dynamic sizing, more than one world, and any ability to describe a game without
|
|
writing C. Those are the tradeoffs. [Chapter 3](03-getting-started.md) puts a window on the
|
|
screen.
|