The pre-commit hook is version controlled under scripts/hooks/, but Git configuration is not cloned, so the hook does nothing until each clone is pointed at it. Nothing said so anywhere a new contributor would look. Cover installation, verification, what the hook actually does, the Emacs requirement, bypassing, and uninstalling. Two things worth calling out explicitly: - core.hooksPath replaces the hooks directory outright rather than adding to it, so any pre-existing .git/hooks entries silently stop firing. Document a symlink alternative for anyone who keeps local hooks. - The documented --check exit codes are 0 conforming, 1 non-conforming, 2 environment failure, so a CI job failing on any non-zero status will not mistake a missing Emacs for a clean tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
377 lines
13 KiB
Markdown
377 lines
13 KiB
Markdown
## How do I initialize a game
|
|
|
|
Initialize the global game object with info about your game
|
|
|
|
```c
|
|
strncpy((char *)&game.name, "sdl3-gametest", 256);
|
|
strncpy((char *)&game.version, "0.0.1", 32);
|
|
strncpy((char *)&game.uri, "net.aklabs.games.sdl3-gametest", 256);
|
|
```
|
|
|
|
Call the game initialization routines and lock the game state for further initialization
|
|
|
|
```c
|
|
PASS(e, akgl_game_init());
|
|
PASS(e, akgl_game_state_lock());
|
|
```
|
|
|
|
If you have a registry properties file, load it. If you don't have a properties file, use `akgl_set_property("prop_name", "prop_value")` to populate the required game properties.
|
|
|
|
```c
|
|
PASS(e, akgl_registry_load_properties(YOUR_REGISTR_FILEPATH));
|
|
```
|
|
|
|
Initialize your physics engine and renderer of choice
|
|
|
|
```c
|
|
PASS(e, akgl_render_init2d(renderer));
|
|
PASS(e, akgl_physics_init_arcade(physics));
|
|
```
|
|
|
|
Unlock the game state
|
|
|
|
```c
|
|
PASS(e, akgl_game_state_unlock());
|
|
```
|
|
|
|
## What is in a properties file (or, What properties must I set if I don't have one?)
|
|
|
|
```json
|
|
{
|
|
"properties": {
|
|
"game.screenwidth": "640",
|
|
"game.screenheight": "480",
|
|
"physics.gravity.y": "1024.0",
|
|
"physics.drag.y": "1.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
Physics properties (gravity and drag along X, Y and Z) are optional and default to `0`.
|
|
|
|
## How do I update and render the game world in my main loop
|
|
|
|
In your game loop (or in your `SDL_AppIterate` method), lock the game state, call the game update function, and then unlock the game state
|
|
|
|
```c
|
|
PASS(e, akgl_game_state_lock());
|
|
PASS(e, renderer->frame_start(renderer));
|
|
SDL_RenderClear(renderer->sdl_renderer);
|
|
PASS(e, akgl_game_update(NULL));
|
|
PASS(e, renderer->frame_end(renderer));
|
|
PASS(e, akgl_game_state_unlock());
|
|
```
|
|
|
|
## How do I get an actor on screen
|
|
|
|
Load a sprite for a character. Sprites are JSON documents describing 2D sprites, frames, and looping. Sprites are named, and sprite names must be unique.
|
|
|
|
```c
|
|
PASS(e, akgl_sprite_load_json(SOME_FILENAME))
|
|
```
|
|
|
|
Load a character from a JSON file. Characters map sprites to actor states and define physics characteristics like movement speed. Each character is named, and character names must be unique.
|
|
|
|
```c
|
|
PASS(e, akgl_character_load_json(SOME_FILENAME))
|
|
```
|
|
|
|
You don't strictly have to load sprites and characters from json files, you can initialize them yourself, it's just tedious work. Here's an example of initializing a 32x32 sprite from a spritesheet that uses the first 4 frames in a looping animation.
|
|
|
|
```c
|
|
akgl_SpriteSheet *sheet;
|
|
akgl_Sprite *sprite;
|
|
akgl_Character *character;
|
|
PASS(e, akgl_heap_next_spritesheet(&sheet);
|
|
PASS(e, akgl_spritesheet_initialize(sheet, 32, 32, IMAGE_FILENAME));
|
|
PASS(e, akgl_heap_next_sprite(&sprite));
|
|
PASS(e, akgl_sprite_initialize(sprite, SPRITE_NAME, &sheet);
|
|
sprite->frames = 4;
|
|
sprite->frameids = [0, 1, 2, 3];
|
|
sprite->width = 32;
|
|
sprite->height = 32;
|
|
sprite->speed = 1000;
|
|
sprite->loop = true;
|
|
strncpy((char *)&sprite->name, "SPRITE NAME", AKGL_SPRITE_MAX_NAME_LENGTH);
|
|
PASS(e, akgl_heap_next_character(&character));
|
|
PASS(e, akgl_character_initialize(&character, "CHAR NAME"));
|
|
PASS(e, akgl_character_sprite_add(&character, &sprite, STATE_MASK));
|
|
// Set the character acceleration and scale if desired
|
|
character->ax = 64.0;
|
|
character->ay = 64.0;
|
|
character->sx = 2.0;
|
|
character->sy = 2.0;
|
|
```
|
|
|
|
Initialize an actor. Actors are named ("player", "Quest NPC", whatever) and names must be unique.
|
|
|
|
```c
|
|
akgl_Actor *myactor = NULL;
|
|
PASS(e, akgl_heap_next_actor(&myactor);
|
|
PASS(e, akgl_actor_initialize(&myactor, "ACTOR_NAME"));
|
|
```
|
|
|
|
Assign a character to the actor by looking up the `akgl_Character` from the AKGL registry and assign it.
|
|
|
|
```c
|
|
myactor->basechar = SDL_GetPointerProperty(
|
|
AKGL_REGISTRY_CHARACTER,
|
|
"CHARACTER_NAME",
|
|
NULL);
|
|
FAIL_ZERO_BREAK(e, myactor->basechar, AKERR_REGISTRY, "Character missing");
|
|
```
|
|
|
|
Give the actor a position and a state, and turn it visible.
|
|
|
|
```c
|
|
myactor->state = 9AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT);
|
|
myactor->x = 320;
|
|
myactor->y = 240;
|
|
myactor->visible = true;
|
|
```
|
|
|
|
## What are in Sprite and Character files
|
|
|
|
Sprite files:
|
|
|
|
```json
|
|
{
|
|
"spritesheet": {
|
|
"filename": "RELATIVE_IMAGE_FILE_REFERENCE",
|
|
"frame_width": int,
|
|
"frame_height": int
|
|
},
|
|
"name": "UNIQUE_SPRITE_NAME",
|
|
"width": int,
|
|
"height": int,
|
|
"speed": int,
|
|
"loop": boolean,
|
|
"loopReverse": boolean,
|
|
"frames": [
|
|
int
|
|
]
|
|
}
|
|
```
|
|
|
|
* `frames` references the frame indexes in the spritesheet that should be used for this animation. Spritesheets are counted from the top left corner going to the right according to the spritesheet `frame_width` and `frame_height`.
|
|
* `loop` says whether or not we should loop the animation
|
|
* `loopReverse` says whether or not we should "bounce" the animation (when we reach the end of the frames, start counting back to the beginning, then count to the end, etc). Otherwise the frames are displayed from 0..n and then cycles back to 0.
|
|
* `speed` is the number of milliseconds each frame in the animation should appear on the screen
|
|
|
|
Character files:
|
|
|
|
```c
|
|
{
|
|
"name": "UNIQUE_CHARACTER_NAME",
|
|
"speedtime": 8,
|
|
"speed_x": 0,
|
|
"speed_y": 0,
|
|
"acceleration_x": 0,
|
|
"acceleration_y": 0,
|
|
"sprite_mappings": [
|
|
{
|
|
"state": [
|
|
"AKGL_ACTOR_STATE_ALIVE",
|
|
"AKGL_ACTOR_STATE_FACE_UP",
|
|
"AKGL_ACTOR_STATE_MOVING_UP"
|
|
],
|
|
"sprite": "menupointer"
|
|
}[, ...]
|
|
]
|
|
}
|
|
```
|
|
|
|
* `speedtime` appears to be legacy and unused.
|
|
* `speed_[xy]` and `acceleration_[xy]` are physics parameters that specify the top speed and acceleration rate (in pixels per nanosecond) of the character in physics simulations. The effect of acceleration depends on the physics simulation being used at the time (which may or may not account for gravity, drag, etc).
|
|
* `sprite_mappings` map a set of actor state flag bitmasks (assume everything in `state` is `OR`ed together) to a sprite name. The game engine uses this to automatically pick the correct sprite (by name) for a given set of state flags. You need one of these for every possible state the character may be used in.
|
|
|
|
## How do I load a tilemap from the filesystem and display it on screen with my actors
|
|
|
|
The engine ONLY supports TilED TMJ tilemaps with tileset external references. Load a tilemap into the global `akgl_Tilemap *gamemap` object.
|
|
|
|
```c
|
|
PASS(e, akgl_tilemap_load(PATHSTRING, gamemap));
|
|
```
|
|
|
|
Actors will be automatically populated from objects in the tilemap object layers. Actor state flags here must be expressed as an integer, you can't (yet) use the same array of strings that is used in character json files.
|
|
|
|
```json
|
|
"objects":[
|
|
{
|
|
"gid":147,
|
|
"height":16,
|
|
"id":1,
|
|
"name":"player",
|
|
"properties":[
|
|
{
|
|
"name":"character",
|
|
"type":"string",
|
|
"value":"little guy"
|
|
},
|
|
{
|
|
"name":"state",
|
|
"type":"int",
|
|
"value":24
|
|
}],
|
|
"rotation":0,
|
|
"type":"actor",
|
|
"visible":true,
|
|
"width":16,
|
|
"x":440.510088317656,
|
|
"y":140.347239175702
|
|
}[, ... ]
|
|
```
|
|
|
|
Check if the tilemap wants to use its own physics, and if you want to allow that, override the global physics simulation
|
|
|
|
```c
|
|
if ( gamemap->use_own_physics == true ) {
|
|
physics = &gamemap->physics;
|
|
}
|
|
```
|
|
|
|
Tilemap physics specification follows. A map can specify its own physics properties (drag, gravity) without specifying a custom model.
|
|
|
|
```json
|
|
"properties":[
|
|
{
|
|
"name":"physics.drag.y",
|
|
"type":"float",
|
|
"value":0
|
|
},
|
|
{
|
|
"name":"physics.gravity.y",
|
|
"type":"float",
|
|
"value":0
|
|
},
|
|
{
|
|
"name":"physics.model",
|
|
"type":"string",
|
|
"value":"arcade"
|
|
}],
|
|
```
|
|
|
|
The global `gamemap` object is automatically displayed if it is populated. Actors are drawn at the appropriate map layer depending on the actor's `layer` property (warning: this may be replaced with a `z` property soon.)
|
|
|
|
## How do I get the screen width and height
|
|
|
|
The most direct is to call `SDL_GetCurrentDisplayMode` to get the parameters from the returned `SDL_DisplayMode` structure (`->w` and `->h`).
|
|
|
|
The simplest way is to check the global `camera` object's `camera->w` and `camera->h` object. You may have more than one camera on a scene, and it's theoretically possible that the global camera object has been overriden and no longer represents the full screen.
|
|
|
|
The most reliable engine-centric way is to use `akgl_get_property` to get the property from the engine. Properties are read and stored as strings, so if you need to do these kinds of things a lot, cache the integer value somewhere.
|
|
|
|
```c
|
|
akgl_String *width = NULL;
|
|
int screenwidth = NULL;
|
|
PASS(e, akgl_get_property("game.screenwidth", &width, "0"));
|
|
PASS(e, aksl_atoi(width->data, &screenwidth));
|
|
PASS(e, akgl_heap_release_string(width));
|
|
```
|
|
|
|
|
|
## Git hooks
|
|
|
|
The repository ships a `pre-commit` hook that keeps committed C sources in the
|
|
project's canonical format (Emacs `cc-mode` "stroustrup"; see `AGENTS.md` for the
|
|
full style guide). The hook lives in `scripts/hooks/` and is version controlled,
|
|
but **Git configuration is not cloned**, so every clone has to be pointed at it
|
|
once:
|
|
|
|
```sh
|
|
git config core.hooksPath scripts/hooks
|
|
```
|
|
|
|
Confirm it took effect:
|
|
|
|
```sh
|
|
git rev-parse --git-path hooks # should print: scripts/hooks
|
|
```
|
|
|
|
That is the whole installation. The hook is already committed with its
|
|
executable bit set, so nothing needs `chmod`.
|
|
|
|
### What the hook does
|
|
|
|
On each commit it looks at the **staged** content of any added, copied, modified,
|
|
or renamed `.c`/`.h` file under `src/`, `include/`, `tests/`, or `util/`, and
|
|
reindents it if it does not already match the canonical style. Checking the
|
|
staged content rather than the working tree means what lands in the commit is
|
|
what was actually verified.
|
|
|
|
When a file needs reindenting, the hook fixes it in the working tree and
|
|
re-stages it — but only when the index and working tree agree for that file. If
|
|
they differ, you have staged part of a file with `git add -p`, and re-staging
|
|
would sweep your unstaged work into the commit. Rather than do that silently the
|
|
hook stops and prints the commands to run yourself:
|
|
|
|
```sh
|
|
scripts/reindent.sh path/to/file.c
|
|
git add path/to/file.c
|
|
```
|
|
|
|
`include/akgl/SDL_GameControllerDB.h` is generated and always skipped.
|
|
|
|
### Requirements
|
|
|
|
The hook drives Emacs in batch mode, because `cc-mode`'s indentation engine is
|
|
the definition of the style and `clang-format` cannot reproduce it exactly. If
|
|
`emacs` is not on `PATH` the hook prints a warning and allows the commit — a
|
|
hook that refuses to run without an optional tool only teaches people to reach
|
|
for `--no-verify`. Install Emacs to get the check; without it, formatting is on
|
|
you.
|
|
|
|
### Bypassing and uninstalling
|
|
|
|
Skip the hook for a single commit:
|
|
|
|
```sh
|
|
git commit --no-verify
|
|
```
|
|
|
|
Remove it entirely:
|
|
|
|
```sh
|
|
git config --unset core.hooksPath
|
|
```
|
|
|
|
### If you already have local hooks
|
|
|
|
`core.hooksPath` **replaces** the hooks directory outright — once it is set, Git
|
|
stops reading `.git/hooks/` altogether, so any hooks you keep there will silently
|
|
stop firing. If that matters, leave `core.hooksPath` unset and symlink just this
|
|
one hook instead:
|
|
|
|
```sh
|
|
ln -s ../../scripts/hooks/pre-commit .git/hooks/pre-commit
|
|
```
|
|
|
|
### Formatting without the hook
|
|
|
|
The hook is a convenience, not the source of truth. The same check is available
|
|
directly, and is what you would run in CI:
|
|
|
|
```sh
|
|
scripts/reindent.sh --check # list non-conforming files; exit 1 if any
|
|
scripts/reindent.sh # reindent every tracked C source in place
|
|
scripts/reindent.sh src/game.c # reindent specific files
|
|
```
|
|
|
|
`--check` exits `0` when everything conforms, `1` when a file needs reindenting,
|
|
and `2` if Emacs is missing or the script cannot run — so a CI job that treats
|
|
any non-zero status as failure will not mistake a broken toolchain for a clean
|
|
tree.
|
|
|
|
## Mutation testing
|
|
|
|
The mutation harness makes one deliberate source-code change at a time in a scratch copy, then rebuilds and runs the passing CTest suite to measure whether tests detect the change. The known-failing `character` test is excluded by default.
|
|
|
|
```sh
|
|
cmake --build build --target mutation
|
|
scripts/mutation_test.py --target src/tilemap.c --list
|
|
scripts/mutation_test.py --target src/tilemap.c --max-mutants 10
|
|
scripts/mutation_test.py --threshold 40 --junit mutation-junit.xml
|
|
```
|
|
|
|
The default run covers all libakgl-owned files under `src/`. Use repeated `--target` options to narrow the scope. A surviving mutant identifies behavior that the current tests do not verify; the script prints its file, line, operator, and exact edit. The real working tree is never mutated.
|