Delete the JRPG's collision too, and wall the map with proxies
cell_solid and feet_blocked go, and so does the prediction in the player's movementlogicfunc. That prediction was only ever exact because the town has zero gravity and zero drag -- v is t, so `x + tx * dt` is where the step lands. A map with gravity would have had to fold ey in as well, which is the game re-implementing the integrator. Resolution runs after the move now, so there is nothing to predict. The map's decoration layer carries `collidable`, which retires JRPG_LAYER_SOLID: akgl_TilemapLayer has no name member, so the game and the map had to agree on an index out of band and inserting a layer in Tiled broke it. The edge of the world is not on any layer, so it is four static proxies covering the outer ring plus a tile of overhang -- one pool slot per side instead of a hundred solid tiles, and the first use of the static-proxy path in either example. They carry LAYER_STATIC explicitly: shape_box defaults a shape to LAYER_ACTOR, and a wall left on that layer is a wall everything walks through. Only the player gets a shape. NPCs stand still and are spoken to; the follower is a child snapped to its parent every step. Verified against the old implementation with the demo script rewritten to hold one direction: holding left into a building stops the player at x=122 with both, and holding up into the map's edge stops them at y=-4 with both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
# 20. Tutorial: a 2D sidescroller
|
# 20. Tutorial: a 2D sidescroller
|
||||||
|
|
||||||
A complete game: a Tiled level, a player who runs and jumps, platforms that hold him up,
|
A complete game: a Tiled level, a player who runs and jumps, platforms that hold him up,
|
||||||
four coins to collect, a blob that patrols and a moth that flies. It is about nine hundred
|
four coins to collect, a blob that patrols and a moth that flies. It is a header and three
|
||||||
lines of C — a header and four translation units, and rather more comment than that — and it
|
translation units, rather more comment than code, and it builds and runs as part of this
|
||||||
builds and runs as part of this repository's ordinary `ctest` run.
|
repository's ordinary `ctest` run.
|
||||||
|
|
||||||
It is here for one reason above the others. **libakgl has no collision detection at all**,
|
It used to be four translation units. The fourth was 383 lines of collision, and it is gone
|
||||||
and a sidescroller is the shortest path to finding that out. So this chapter is mostly about
|
— which makes this chapter as much about **where in the frame a thing has to happen** as
|
||||||
what you write when the engine stops, and where exactly that code has to live for the frame
|
about what to write. That is the lesson that survived: the same test, run a few lines
|
||||||
to come out right.
|
earlier, is wrong in ways that take an afternoon to diagnose.
|
||||||
|
|
||||||
The program is `examples/sidescroller/`; the art, the map and the JSON are in
|
The program is `examples/sidescroller/`; the art, the map and the JSON are in
|
||||||
`docs/tutorials/assets/sidescroller/`. Every listing below is quoted straight out of those
|
`docs/tutorials/assets/sidescroller/`. Every listing below is quoted straight out of those
|
||||||
|
|||||||
@@ -198,72 +198,105 @@ and `akgl_actor_cmhf_*_off` clears only the movement bit on the way up — so th
|
|||||||
facing an actor is left with is exactly the one it was walking in. The automatic
|
facing an actor is left with is exactly the one it was walking in. The automatic
|
||||||
`facefunc` is for an actor whose facing is not otherwise decided.
|
`facefunc` is for an actor whose facing is not otherwise decided.
|
||||||
|
|
||||||
## Walls, because the library has none
|
## Walls
|
||||||
|
|
||||||
There is no collision in libakgl. Not "a simple one" — none:
|
The library resolves them ([Chapter 15](15-collision.md)); this game says which
|
||||||
|
things are solid and gets out of the way.
|
||||||
|
|
||||||
- `akgl_physics_arcade_collide` raises `AKERR_API` with the message
|
Two rules, and they arrive by two different mechanisms because they are two
|
||||||
`"Not implemented"`.
|
different kinds of geometry. Every non-empty cell of the `decoration` layer — a
|
||||||
- `akgl_physics_simulate` never calls `collide` at all, so that status is
|
building, a tree, a lamp post — is solid, and the layer says so itself:
|
||||||
unreachable through the normal path.
|
|
||||||
- `akgl_physics_arcade_move` is `x += vx * dt` on three axes and consults no
|
|
||||||
tilemap, no other actor and no bound of any kind.
|
|
||||||
|
|
||||||
An actor therefore walks through a building and off the edge of the world.
|
```json excerpt=docs/tutorials/assets/jrpg/town.tmj
|
||||||
[Chapter 14](14-physics.md) lists this among what is not implemented. A game
|
"properties": [
|
||||||
that wants a wall writes one, and the place to write it is the actor's
|
{
|
||||||
`movementlogicfunc` — the one hook the physics step calls *before* it commits
|
"name": "collidable",
|
||||||
|
"type": "bool",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
That replaces a `#define JRPG_LAYER_SOLID 1`, and the replacement matters more
|
||||||
|
than it looks. **`akgl_TilemapLayer` has no `name` member** — the loader reads a
|
||||||
|
layer's `id`, `type`, `opacity`, `visible` and offset and drops the name Tiled
|
||||||
|
wrote, so a game could not say "the layer called decoration" and had to agree
|
||||||
|
with the map on an *index*. Insert a layer in Tiled and the game starts colliding
|
||||||
|
with the scenery. Now the map carries the answer and the game names no layer at
|
||||||
|
all.
|
||||||
|
|
||||||
|
The second rule is the edge of the world, which is not on any layer. That is four
|
||||||
|
static proxies:
|
||||||
|
|
||||||
|
```c excerpt=examples/jrpg/world.c
|
||||||
|
PASS(errctx, akgl_heap_next_collision_proxy(&jrpg_edges[i]));
|
||||||
|
PASS(errctx, akgl_collision_shape_box(&jrpg_edge_shapes[i], &sides[i], 0.0f));
|
||||||
|
jrpg_edge_shapes[i].layermask = AKGL_COLLISION_LAYER_STATIC;
|
||||||
|
jrpg_edge_shapes[i].collidemask = AKGL_COLLISION_LAYER_NONE;
|
||||||
|
jrpg_edge_shapes[i].flags |= AKGL_COLLISION_FLAG_STATIC;
|
||||||
|
```
|
||||||
|
|
||||||
|
Four proxies rather than a hundred solid tiles, because a long thin box costs one
|
||||||
|
pool slot whatever its length — this is what proxies are for, and tiles are the
|
||||||
|
other mechanism precisely because there are a hundred thousand of them.
|
||||||
|
|
||||||
|
Three details in five lines:
|
||||||
|
|
||||||
|
- **`layermask` is set by hand.** `akgl_collision_shape_box` defaults a shape to
|
||||||
|
`AKGL_COLLISION_LAYER_ACTOR`, which is right for an actor and wrong for
|
||||||
|
scenery. An actor responds to `AKGL_COLLISION_LAYER_STATIC` and nothing else,
|
||||||
|
so a wall left on the default layer is a wall everything walks through.
|
||||||
|
- **`collidemask` is cleared.** The wall is not asking to be pushed out of
|
||||||
anything.
|
anything.
|
||||||
|
- **The acquire and the initialize are adjacent, with nothing between them.**
|
||||||
|
`akgl_heap_next_collision_proxy` finds a free slot and does *not* claim it; the
|
||||||
|
initialize takes the reference. Between the two lines a second acquire returns
|
||||||
|
the same pointer. [Chapter 5](05-the-heap.md) covers why the pools are built
|
||||||
|
that way.
|
||||||
|
|
||||||
The rule this game applies is that the outer ring of the map is solid and every
|
The walls cover the map's outermost ring of cells *and* extend a tile beyond it.
|
||||||
non-empty cell of the `decoration` layer is solid:
|
The ring is what the art expects; the overhang is what stops an actor arriving
|
||||||
|
fast enough from being resolved out the far side of a wall exactly one tile
|
||||||
|
thick.
|
||||||
|
|
||||||
|
Then the player gets a footprint:
|
||||||
|
|
||||||
```c excerpt=examples/jrpg/world.c
|
```c excerpt=examples/jrpg/world.c
|
||||||
if ( (tx <= 0) || (ty <= 0) ||
|
body = (SDL_FRect){ FEET_X, FEET_TOP, FEET_W, FEET_H };
|
||||||
(tx >= (akgl_gamemap->width - 1)) || (ty >= (akgl_gamemap->height - 1)) ) {
|
PASS(errctx, akgl_collision_shape_box(&player->shape, &body, 0.0f));
|
||||||
return true;
|
player->shape_override = true;
|
||||||
}
|
|
||||||
return (akgl_gamemap->layers[JRPG_LAYER_SOLID].data[(ty * akgl_gamemap->width) + tx] != 0);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`JRPG_LAYER_SOLID` is the number 1, not the string `"decoration"`, and that is
|
A 20×10 box at the bottom of the 32×32 frame rather than the whole frame, because
|
||||||
the second workaround. **`akgl_TilemapLayer` has no `name` member.** The loader
|
a character stands on their feet and every doorway in the town is one tile wide.
|
||||||
reads a layer's `id`, `type`, `opacity`, `visible` and offset, and drops the
|
Testing the whole frame would make every corridor two tiles wide.
|
||||||
name Tiled wrote — the only `"name"` an object layer keeps is the one on each
|
|
||||||
*object*. So a game cannot say "the layer called collision"; the map and the
|
|
||||||
program agree on an index, and if somebody inserts a layer in Tiled the game
|
|
||||||
starts colliding with the scenery. This is not in `TODO.md`.
|
|
||||||
|
|
||||||
Then the hook itself. Two things make the prediction below exact rather than a
|
**Only the player gets one.** The NPCs stand still and are spoken to, not walked
|
||||||
guess, and both are worth understanding before you copy it:
|
into; shapes on them would make the town a pinball table. The follower is a
|
||||||
|
child, snapped to the player's position every step, so a shape on it would fight
|
||||||
|
the snap rather than stop anything. That is what the default masks are for — an
|
||||||
|
actor with a shape collides with map geometry and with no other actor until you
|
||||||
|
add a bit.
|
||||||
|
|
||||||
- By the time a `movementlogicfunc` runs, `akgl_physics_simulate` has **already**
|
### What went away
|
||||||
integrated this step's thrust and capped it against the character's speed
|
|
||||||
ellipse. `tx` and `ty` are this step's final thrust.
|
|
||||||
- Velocity is recomputed as `e + t` every step, and this map has zero gravity and
|
|
||||||
zero drag, so `e` stays zero and `v` *is* `t`. A map with gravity would have to
|
|
||||||
fold `ey` into the prediction as well.
|
|
||||||
|
|
||||||
```c excerpt=examples/jrpg/world.c
|
This chapter used to carry a `movementlogicfunc` that *predicted* the step:
|
||||||
if ( feet_blocked(obj->x + (obj->tx * dt), obj->y) ) {
|
|
||||||
obj->tx = 0.0f;
|
```text
|
||||||
obj->ax = 0.0f;
|
if ( feet_blocked(obj->x + (obj->tx * dt), obj->y) ) { obj->tx = 0.0f; ... }
|
||||||
}
|
|
||||||
if ( feet_blocked(obj->x, obj->y + (obj->ty * dt)) ) {
|
|
||||||
obj->ty = 0.0f;
|
|
||||||
obj->ay = 0.0f;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
One axis at a time, so walking diagonally into a wall slides along it instead of
|
It worked, and it came with a qualifier: velocity is recomputed as `e + t` every
|
||||||
stopping dead. Zeroing thrust rather than moving the actor back is what keeps
|
step, and it was only exact because this map has zero gravity and zero drag, so
|
||||||
this inside the library's model: the arcade backend will still do its own
|
`e` stays zero and `v` *is* `t`. A map with gravity would have had to fold `ey`
|
||||||
`x += vx * dt` a few lines later, and it will add zero.
|
into the prediction as well — which is the game re-implementing the library's
|
||||||
|
integrator, in a file that would not be recompiled when the integrator changed.
|
||||||
|
|
||||||
`feet_blocked` tests a 20×10 footprint at the bottom of the 32×32 frame rather
|
Resolution runs after the move now, on a position that already happened, so
|
||||||
than the whole frame, because a character stands on their feet and every
|
there is nothing to predict and the qualifier goes away with it. Both this game
|
||||||
doorway in the town is one tile wide. Four corners is enough only because the
|
and the sidescroller land in exactly the same pixel they did before: holding left
|
||||||
footprint is smaller than a tile in both directions.
|
into a building stops the player at x=122, and holding up into the map's edge
|
||||||
|
stops them at y=-4, with either implementation.
|
||||||
|
|
||||||
## Freezing the world with `AKGL_ERR_LOGICINTERRUPT`
|
## Freezing the world with `AKGL_ERR_LOGICINTERRUPT`
|
||||||
|
|
||||||
@@ -639,18 +672,18 @@ jrpg: 320 frames, player at (280, 146)
|
|||||||
|
|
||||||
## What this costs
|
## What this costs
|
||||||
|
|
||||||
Six things in this program exist because the library does not do them. None of
|
Four things in this program exist because the library does not do them. None of
|
||||||
them is hidden, and none of them is a criticism of a design — they are the
|
them is hidden, and none of them is a criticism of a design — they are the
|
||||||
current state of a library that is honest about being unfinished.
|
current state of a library that is honest about being unfinished. Two more were
|
||||||
|
here in 0.7.0 and are gone: the hard-coded collision layer index, and the
|
||||||
|
tile-based blocking in the player's `movementlogicfunc`. Collision closed both.
|
||||||
|
|
||||||
| # | Gap | What this game does | Recorded in |
|
| # | Gap | What this game does | Recorded in |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | `movement_controls_face` erases the facing bit of any actor that is not moving, so every map-spawned actor stops being drawn on frame one | Clears the field on every live actor after loading the map | `actor.h` `@note`; a `TODO` in `src/actor.c`. Not in `TODO.md` |
|
| 1 | `movement_controls_face` erases the facing bit of any actor that is not moving, so every map-spawned actor stops being drawn on frame one | Clears the field on every live actor after loading the map | `actor.h` `@note`; a `TODO` in `src/actor.c`. Not in `TODO.md` |
|
||||||
| 2 | `akgl_TilemapLayer` has no `name`, so a collision layer cannot be found by name | Hard-codes the layer index in `JRPG_LAYER_SOLID` | Not recorded anywhere |
|
| 2 | `akgl_default_physics` is zeroed BSS and `akgl_game_init` never calls the factory; `akgl_game_update` calls `simulate` through a `NULL` pointer | Calls `akgl_physics_init_arcade` explicitly, and switches to the map's backend when it declares one | The false `physics.engine` claim in `physics.h` is in the plan's out-of-scope list |
|
||||||
| 3 | `akgl_default_physics` is zeroed BSS and `akgl_game_init` never calls the factory; `akgl_game_update` calls `simulate` through a `NULL` pointer | Calls `akgl_physics_init_arcade` explicitly, and switches to the map's backend when it declares one | The false `physics.engine` claim in `physics.h` is in the plan's out-of-scope list |
|
| 3 | A child actor's position is written as absolute by the physics step and read as relative by the renderer, so the parent's position is added twice | A `renderfunc` that detaches the parent for the duration of the draw | **Not in `TODO.md`.** `actor.h` documents both readings |
|
||||||
| 4 | No collision of any kind: `arcade_collide` raises `AKERR_API`, `simulate` never calls it, `arcade_move` clamps nothing | Tile-based blocking in the player's `movementlogicfunc` | `TODO.md`; [Chapter 14](14-physics.md) |
|
| 4 | `akgl_tilemap_release` double-frees tileset textures and never frees image-layer ones | Does not call it; lets `SDL_Quit` reclaim them | `TODO.md`, "Known and still open" item 2 |
|
||||||
| 5 | A child actor's position is written as absolute by the physics step and read as relative by the renderer, so the parent's position is added twice | A `renderfunc` that detaches the parent for the duration of the draw | **Not in `TODO.md`.** `actor.h` documents both readings |
|
|
||||||
| 6 | `akgl_tilemap_release` double-frees tileset textures and never frees image-layer ones | Does not call it; lets `SDL_Quit` reclaim them | `TODO.md`, "Known and still open" item 2 |
|
|
||||||
|
|
||||||
Two more that are not workarounds but will bite anyone extending this: `akgl_Actor::layer`
|
Two more that are not workarounds but will bite anyone extending this: `akgl_Actor::layer`
|
||||||
is a `uint32_t` with no bound, while `akgl_render_2d_draw_world` stops at
|
is a `uint32_t` with no bound, while `akgl_render_2d_draw_world` stops at
|
||||||
|
|||||||
@@ -1227,7 +1227,14 @@
|
|||||||
"visible": true,
|
"visible": true,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0
|
"y": 0,
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "collidable",
|
||||||
|
"type": "bool",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"draworder": "topdown",
|
"draworder": "topdown",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include <akgl/actor.h>
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/collision.h>
|
||||||
#include <akgl/types.h>
|
#include <akgl/types.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -49,16 +50,6 @@
|
|||||||
/** @brief Point size the font is rasterized at. A size is baked into the handle. */
|
/** @brief Point size the font is rasterized at. A size is baked into the handle. */
|
||||||
#define JRPG_FONT_SIZE 12
|
#define JRPG_FONT_SIZE 12
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Index of the tile layer this game treats as solid.
|
|
||||||
*
|
|
||||||
* An index, not a name, because akgl_TilemapLayer has no `name` member: the
|
|
||||||
* loader reads a layer's `id`, `type`, `opacity`, `visible`, offset and data,
|
|
||||||
* and drops the name Tiled wrote. A map cannot say "the layer called
|
|
||||||
* collision", so the game and the map agree on a number. See chapter 20.
|
|
||||||
*/
|
|
||||||
#define JRPG_LAYER_SOLID 1
|
|
||||||
|
|
||||||
/** @brief Longest line an NPC can say, terminator included. */
|
/** @brief Longest line an NPC can say, terminator included. */
|
||||||
#define JRPG_TEXTBOX_MAX_TEXT 256
|
#define JRPG_TEXTBOX_MAX_TEXT 256
|
||||||
|
|
||||||
@@ -89,6 +80,15 @@ typedef struct {
|
|||||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_load(void);
|
akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_load(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The collision world, owned by world.c.
|
||||||
|
*
|
||||||
|
* The physics backend resolves through it after every sub-move. Nothing else in
|
||||||
|
* the game touches it: the town is walls and people, and the walls are the
|
||||||
|
* map's own decoration layer plus four proxies around the edge.
|
||||||
|
*/
|
||||||
|
extern akgl_CollisionWorld jrpg_collision;
|
||||||
/**
|
/**
|
||||||
* @brief Fix up the actors the map spawned, and attach the party member.
|
* @brief Fix up the actors the map spawned, and attach the party member.
|
||||||
*
|
*
|
||||||
@@ -108,13 +108,13 @@ akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_populate(void);
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_camera_follow(void);
|
akerr_ErrorContext AKERR_NOIGNORE *jrpg_camera_follow(void);
|
||||||
/**
|
/**
|
||||||
* @brief The player's `movementlogicfunc`: the library default, plus walls.
|
* @brief The player's `movementlogicfunc`: the library default, plus the freeze.
|
||||||
*
|
*
|
||||||
* libakgl implements no collision at all -- akgl_physics_arcade_collide raises
|
* Walls are the library's now -- see chapter 15 -- so all this adds to
|
||||||
* AKERR_API, akgl_physics_simulate never calls `collide`, and
|
* akgl_actor_logic_movement is raising AKGL_ERR_LOGICINTERRUPT while the text
|
||||||
* akgl_physics_arcade_move consults no tilemap -- so a game that wants a wall
|
* box is open, which is the documented way to tell the simulator to skip an
|
||||||
* writes one here. Raises AKGL_ERR_LOGICINTERRUPT while the text box is open,
|
* actor's tick. That also skips `collide`, since the CATCH around
|
||||||
* which is the documented way to tell the simulator to skip an actor's tick.
|
* movementlogicfunc jumps past the whole rest of the step.
|
||||||
*
|
*
|
||||||
* @param obj The actor to compute acceleration for. Required, along with its
|
* @param obj The actor to compute acceleration for. Required, along with its
|
||||||
* `basechar`.
|
* `basechar`.
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
* Almost nothing here is game logic. It is the four steps between a directory
|
* Almost nothing here is game logic. It is the four steps between a directory
|
||||||
* of JSON and a world with people standing in it, in the one order that works,
|
* of JSON and a world with people standing in it, in the one order that works,
|
||||||
* plus the two hooks -- a `movementlogicfunc` and a `renderfunc` -- that a game
|
* plus the two hooks -- a `movementlogicfunc` and a `renderfunc` -- that a game
|
||||||
* has to supply because the library does not.
|
* has to supply because the library does not, and the collision world the
|
||||||
|
* library resolves through.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
|
|
||||||
#include <akgl/actor.h>
|
#include <akgl/actor.h>
|
||||||
#include <akgl/character.h>
|
#include <akgl/character.h>
|
||||||
|
#include <akgl/collision.h>
|
||||||
#include <akgl/error.h>
|
#include <akgl/error.h>
|
||||||
#include <akgl/game.h>
|
#include <akgl/game.h>
|
||||||
#include <akgl/heap.h>
|
#include <akgl/heap.h>
|
||||||
@@ -92,10 +94,26 @@ static jrpg_Townsfolk TOWNSFOLK[] = {
|
|||||||
#define FEET_TOP 20.0f
|
#define FEET_TOP 20.0f
|
||||||
#define FEET_H 10.0f
|
#define FEET_H 10.0f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief How far past the edge of the map the boundary walls extend, in pixels.
|
||||||
|
*
|
||||||
|
* The walls cover the map's outer ring of cells *and* this much beyond it. The
|
||||||
|
* ring is what the old hand-rolled rule made solid and what the art is drawn to
|
||||||
|
* expect; the overhang is what stops an actor arriving fast enough from being
|
||||||
|
* resolved out the far side of a wall exactly one tile thick.
|
||||||
|
*/
|
||||||
|
#define EDGE_OVERHANG 16.0f
|
||||||
|
|
||||||
/** @brief Where the party member walks, in pixels relative to the player. */
|
/** @brief Where the party member walks, in pixels relative to the player. */
|
||||||
#define FOLLOWER_OFFSET_X (-14.0f)
|
#define FOLLOWER_OFFSET_X (-14.0f)
|
||||||
#define FOLLOWER_OFFSET_Y (10.0f)
|
#define FOLLOWER_OFFSET_Y (10.0f)
|
||||||
|
|
||||||
|
akgl_CollisionWorld jrpg_collision;
|
||||||
|
|
||||||
|
/** @brief The four map-edge walls, and the shapes they were built from. */
|
||||||
|
static akgl_CollisionProxy *jrpg_edges[4];
|
||||||
|
static akgl_CollisionShape jrpg_edge_shapes[4];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Load one sprite definition by its three naming components.
|
* @brief Load one sprite definition by its three naming components.
|
||||||
*
|
*
|
||||||
@@ -154,6 +172,62 @@ static akerr_ErrorContext *character_load(char *cast)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Wall off the edges of the map with four static proxies.
|
||||||
|
*
|
||||||
|
* The town's decoration layer says what is solid -- a building, a tree, a lamp
|
||||||
|
* post -- and the map's edge is not on it. Nothing else stops an actor walking
|
||||||
|
* off the edge of the world.
|
||||||
|
*
|
||||||
|
* Four proxies rather than a hundred solid tiles, because static geometry that
|
||||||
|
* is not tile-shaped is exactly what a proxy is for: a long thin box costs one
|
||||||
|
* slot whatever its length. `layermask` is set by hand because
|
||||||
|
* akgl_collision_shape_box defaults a shape to LAYER_ACTOR, which is right for
|
||||||
|
* an actor and wrong for scenery -- an actor responds to LAYER_STATIC and would
|
||||||
|
* walk straight through these.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_HEAP If the proxy pool is exhausted.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *wall_off_the_edges(void)
|
||||||
|
{
|
||||||
|
SDL_FRect sides[4];
|
||||||
|
float32_t ow = 0.0f; /* Map width, plus the overhang at each end. */
|
||||||
|
float32_t oh = 0.0f; /* Map height, the same. */
|
||||||
|
float32_t tw = 0.0f; /* One tile of ring, plus one overhang. */
|
||||||
|
float32_t th = 0.0f;
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ow = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth) + (2.0f * EDGE_OVERHANG);
|
||||||
|
oh = (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight) + (2.0f * EDGE_OVERHANG);
|
||||||
|
tw = (float32_t)akgl_gamemap->tilewidth + EDGE_OVERHANG;
|
||||||
|
th = (float32_t)akgl_gamemap->tileheight + EDGE_OVERHANG;
|
||||||
|
|
||||||
|
/* x y w h */
|
||||||
|
sides[0] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, tw, oh }; /* west */
|
||||||
|
sides[1] = (SDL_FRect){ (ow - tw - EDGE_OVERHANG), -EDGE_OVERHANG, tw, oh }; /* east */
|
||||||
|
sides[2] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, ow, th }; /* north */
|
||||||
|
sides[3] = (SDL_FRect){ -EDGE_OVERHANG, (oh - th - EDGE_OVERHANG), ow, th }; /* south */
|
||||||
|
|
||||||
|
for ( i = 0; i < 4; i++ ) {
|
||||||
|
/*
|
||||||
|
* Acquire and initialize adjacent, with nothing between them: until
|
||||||
|
* akgl_collision_proxy_initialize takes the reference the slot is still
|
||||||
|
* free, and the next acquire hands out the same pointer.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akgl_heap_next_collision_proxy(&jrpg_edges[i]));
|
||||||
|
PASS(errctx, akgl_collision_shape_box(&jrpg_edge_shapes[i], &sides[i], 0.0f));
|
||||||
|
jrpg_edge_shapes[i].layermask = AKGL_COLLISION_LAYER_STATIC;
|
||||||
|
jrpg_edge_shapes[i].collidemask = AKGL_COLLISION_LAYER_NONE;
|
||||||
|
jrpg_edge_shapes[i].flags |= AKGL_COLLISION_FLAG_STATIC;
|
||||||
|
PASS(errctx, akgl_collision_proxy_initialize(jrpg_edges[i], NULL, &jrpg_edge_shapes[i],
|
||||||
|
0.0f, 0.0f, 0.0f));
|
||||||
|
PASS(errctx, jrpg_collision.partitioner.insert(&jrpg_collision.partitioner, jrpg_edges[i]));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *jrpg_world_load(void)
|
akerr_ErrorContext *jrpg_world_load(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -192,6 +266,16 @@ akerr_ErrorContext *jrpg_world_load(void)
|
|||||||
akgl_physics = &akgl_gamemap->physics;
|
akgl_physics = &akgl_gamemap->physics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collision is opt-in, and this is the whole of turning it on. The cell
|
||||||
|
// arguments are placeholders: akgl_collision_bind_tilemap overwrites them
|
||||||
|
// from the map's own tile size, and reads which layers are solid off each
|
||||||
|
// layer's `collidable` property rather than an index the game and the map
|
||||||
|
// had to agree on out of band.
|
||||||
|
PASS(errctx, akgl_collision_world_init(&jrpg_collision, NULL, 16.0f, 16.0f));
|
||||||
|
PASS(errctx, akgl_collision_bind_tilemap(&jrpg_collision, akgl_gamemap));
|
||||||
|
PASS(errctx, wall_off_the_edges());
|
||||||
|
akgl_physics->collision = &jrpg_collision;
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +284,7 @@ akerr_ErrorContext *jrpg_world_populate(void)
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
akgl_Actor *player = NULL;
|
akgl_Actor *player = NULL;
|
||||||
akgl_Actor *follower = NULL;
|
akgl_Actor *follower = NULL;
|
||||||
|
SDL_FRect body;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
// Every actor the map spawned, including the player.
|
// Every actor the map spawned, including the player.
|
||||||
@@ -228,6 +313,15 @@ akerr_ErrorContext *jrpg_world_populate(void)
|
|||||||
);
|
);
|
||||||
player->movementlogicfunc = &jrpg_actor_logic_walk;
|
player->movementlogicfunc = &jrpg_actor_logic_walk;
|
||||||
|
|
||||||
|
// The footprint, and only the player gets one. The NPCs stand still and are
|
||||||
|
// spoken to, not walked into: giving them shapes would make the town a
|
||||||
|
// pinball table and would change what the proximity test in jrpg.c means.
|
||||||
|
// The follower is a child, snapped to the player's position every step, so
|
||||||
|
// a shape on it would fight the snap rather than stop anything.
|
||||||
|
body = (SDL_FRect){ FEET_X, FEET_TOP, FEET_W, FEET_H };
|
||||||
|
PASS(errctx, akgl_collision_shape_box(&player->shape, &body, 0.0f));
|
||||||
|
player->shape_override = true;
|
||||||
|
|
||||||
// The party member. Nothing in the map creates it, and nothing has to: an
|
// The party member. Nothing in the map creates it, and nothing has to: an
|
||||||
// actor is a pool object with a name, and the character it instantiates is
|
// actor is a pool object with a name, and the character it instantiates is
|
||||||
// already registered. This one borrows the elder's template, which is the
|
// already registered. This one borrows the elder's template, which is the
|
||||||
@@ -252,58 +346,6 @@ akerr_ErrorContext *jrpg_world_populate(void)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Is the map cell containing this pixel one the player cannot enter?
|
|
||||||
*
|
|
||||||
* Two rules, and both of them are the game's rather than the library's. The
|
|
||||||
* outer ring of the map is solid, because nothing else stops an actor walking
|
|
||||||
* off the edge of the world. Everything else is the decoration layer: a cell
|
|
||||||
* with a tile in it is a building, a tree or a lamp post, and a cell with 0 in
|
|
||||||
* it is open ground.
|
|
||||||
*
|
|
||||||
* @param px Position in map pixels along x.
|
|
||||||
* @param py Position in map pixels along y.
|
|
||||||
* @return True when the cell blocks movement.
|
|
||||||
*/
|
|
||||||
static bool cell_solid(float32_t px, float32_t py)
|
|
||||||
{
|
|
||||||
int tx = 0;
|
|
||||||
int ty = 0;
|
|
||||||
|
|
||||||
tx = (int)(px / (float32_t)akgl_gamemap->tilewidth);
|
|
||||||
ty = (int)(py / (float32_t)akgl_gamemap->tileheight);
|
|
||||||
|
|
||||||
if ( (tx <= 0) || (ty <= 0) ||
|
|
||||||
(tx >= (akgl_gamemap->width - 1)) || (ty >= (akgl_gamemap->height - 1)) ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return (akgl_gamemap->layers[JRPG_LAYER_SOLID].data[(ty * akgl_gamemap->width) + tx] != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Would an actor whose frame is at (x, y) have its feet in a wall?
|
|
||||||
*
|
|
||||||
* Tests the four corners of the footprint. Four corners is enough only because
|
|
||||||
* the footprint is smaller than a tile in both directions; a bigger one would
|
|
||||||
* step over a single-cell wall between two of its corners.
|
|
||||||
*
|
|
||||||
* @param x Candidate frame position along x, in map pixels.
|
|
||||||
* @param y Candidate frame position along y, in map pixels.
|
|
||||||
* @return True when any corner of the footprint lands in a solid cell.
|
|
||||||
*/
|
|
||||||
static bool feet_blocked(float32_t x, float32_t y)
|
|
||||||
{
|
|
||||||
float32_t left = x + FEET_X;
|
|
||||||
float32_t right = x + FEET_X + FEET_W;
|
|
||||||
float32_t top = y + FEET_TOP;
|
|
||||||
float32_t bottom = y + FEET_TOP + FEET_H;
|
|
||||||
|
|
||||||
return (cell_solid(left, top) ||
|
|
||||||
cell_solid(right, top) ||
|
|
||||||
cell_solid(left, bottom) ||
|
|
||||||
cell_solid(right, bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
akerr_ErrorContext *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt)
|
akerr_ErrorContext *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -335,25 +377,13 @@ akerr_ErrorContext *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt)
|
|||||||
// Everything the default hook does -- re-copy the character's speed limits,
|
// Everything the default hook does -- re-copy the character's speed limits,
|
||||||
// turn the movement bits into signed acceleration -- is still wanted. This
|
// turn the movement bits into signed acceleration -- is still wanted. This
|
||||||
// hook adds to it rather than replacing it.
|
// hook adds to it rather than replacing it.
|
||||||
PASS(errctx, akgl_actor_logic_movement(obj, dt));
|
|
||||||
|
|
||||||
// Where this step would put us. akgl_physics_simulate has already
|
|
||||||
// integrated thrust for this step and capped it against the character's
|
|
||||||
// speed ellipse, and it computes velocity as `e + t` -- and with the town's
|
|
||||||
// zero gravity and zero drag, `e` stays zero, so `v` is `t`. That is what
|
|
||||||
// makes the prediction exact rather than approximate. A map with gravity
|
|
||||||
// would have to account for `ey` here as well.
|
|
||||||
//
|
//
|
||||||
// Axis at a time, so walking into a wall diagonally slides along it rather
|
// Walls used to be here too: a prediction of where this step would land,
|
||||||
// than stopping dead.
|
// tested axis at a time. It was only ever correct because the town has zero
|
||||||
if ( feet_blocked(obj->x + (obj->tx * dt), obj->y) ) {
|
// gravity and zero drag, so `v` is `t` and the prediction is exact. The
|
||||||
obj->tx = 0.0f;
|
// library resolves after the move instead, on a position that already
|
||||||
obj->ax = 0.0f;
|
// happened, and that qualifier goes away with it.
|
||||||
}
|
PASS(errctx, akgl_actor_logic_movement(obj, dt));
|
||||||
if ( feet_blocked(obj->x, obj->y + (obj->ty * dt)) ) {
|
|
||||||
obj->ty = 0.0f;
|
|
||||||
obj->ay = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user