Actor rotation, and collision under it #62

Open
opened 2026-08-02 18:34:52 -04:00 by tachikoma · 0 comments
Collaborator

Source: TODO.md, "Actor rotation, and collision under it" (at bbb7b8f)

No actor carries an angle. akgl_actor_render hard-codes SDL_FLIP_NONE,
every collision shape is axis-aligned, and both example games depend on that being
true.

Adding rotation is a small change in five named places and a large change in one,
and this issue exists so the large one is not a surprise.

What it touches, in the order it would have to be done:

  • akgl_Actor grows an angle, in radians, about z. Render consumes it:
    SDL_RenderTextureRotated takes an angle in degrees and a centre, so
    akgl_actor_render converts and picks a centre. The centre is a decision,
    not a detail
    -- a sprite rotating about its frame centre and one rotating
    about its feet look nothing alike.
  • collision_support rotates dir into local space at the top and rotates
    the answer back. That one function is written so this is the whole
    narrowphase change
    : every arm of its switch already answers in the shape's
    own frame, and the world centre is added by collision_ccdobj rather than
    inside the switch. A ccd_quat_t on collision_ccdobj, one ccdQuatRotVec
    in and one out, and MPR and GJK both work on rotated shapes with nothing else
    touched. collision_center is unaffected.
  • The box fast path stops being valid for a rotated pair.
    collision_box_box is a closed form that assumes both boxes are
    axis-aligned; a rotated pair has to fall through to MPR. That is a branch on
    (a->angle != 0 || b->angle != 0) and a measurable cost -- the fast path is
    2-7x cheaper (PERFORMANCE.md, "Collision, measured"), so a game that
    rotates everything pays for it.
  • The broad-phase bound becomes the rotated bound.
    akgl_collision_shape_bounds returns the shape's own AABB; a rotated shape
    needs the AABB of the rotated shape, which is larger. The function grows an
    angle parameter, and every caller -- akgl_collision_settle, the grid's
    insert/move, ss_grounded in the sidescroller -- passes one. A broad
    phase that keeps returning the unrotated bound under-reports, which is the
    one failure mode the partitioner contract forbids.
  • akgl_collision_proxy_sync copies the angle alongside the shape and the
    position, since that is the one place the proxy's copy is refreshed.

The z-extrusion invariant is unaffected. Rotation is about z, so a shape's
half-extent along z does not change and AKGL_COLLISION_DEPTH_RATIO still makes z
overlap exceed any achievable planar penetration. See
Chapter 15, "Why a 2D shape has a depth".

What it does not touch: the response. akgl_actor_collide_block works on a
normal and a velocity and does not care how the normal was found. Angular velocity,
torque and rotational response are a different piece of work and are not implied
by any of the above -- a rotated shape that resolves linearly is a perfectly
coherent intermediate state, and it is the one a top-down shooter actually wants.

Related: akgl_actor_render hard-coding SDL_FLIP_NONE is filed separately; it is
the same call site and wants deciding at the same time.

Files: include/akgl/actor.h, src/actor.c, src/collision.c, src/collision_grid.c, include/akgl/collision.h


Filed by Tachikoma (Claude Code, Opus 5, 1M context)

**Source:** TODO.md, "Actor rotation, and collision under it" (at bbb7b8f) **No actor carries an angle.** `akgl_actor_render` hard-codes `SDL_FLIP_NONE`, every collision shape is axis-aligned, and both example games depend on that being true. Adding rotation is a small change in five named places and a large change in one, and **this issue exists so the large one is not a surprise.** What it touches, in the order it would have to be done: - [ ] **`akgl_Actor` grows an `angle`**, in radians, about z. Render consumes it: `SDL_RenderTextureRotated` takes an angle in *degrees* and a centre, so `akgl_actor_render` converts and picks a centre. **The centre is a decision, not a detail** -- a sprite rotating about its frame centre and one rotating about its feet look nothing alike. - [ ] **`collision_support` rotates `dir` into local space** at the top and rotates the answer back. That one function is written so this is *the whole narrowphase change*: every arm of its switch already answers in the shape's own frame, and the world centre is added by `collision_ccdobj` rather than inside the switch. A `ccd_quat_t` on `collision_ccdobj`, one `ccdQuatRotVec` in and one out, and MPR and GJK both work on rotated shapes with nothing else touched. `collision_center` is unaffected. - [ ] **The box fast path stops being valid for a rotated pair.** `collision_box_box` is a closed form that assumes both boxes are axis-aligned; a rotated pair has to fall through to MPR. That is a branch on `(a->angle != 0 || b->angle != 0)` and a measurable cost -- the fast path is 2-7x cheaper (`PERFORMANCE.md`, "Collision, measured"), so a game that rotates everything pays for it. - [ ] **The broad-phase bound becomes the rotated bound.** `akgl_collision_shape_bounds` returns the shape's own AABB; a rotated shape needs the AABB *of the rotated shape*, which is larger. The function grows an angle parameter, and every caller -- `akgl_collision_settle`, the grid's `insert`/`move`, `ss_grounded` in the sidescroller -- passes one. **A broad phase that keeps returning the unrotated bound under-reports, which is the one failure mode the partitioner contract forbids.** - [ ] **`akgl_collision_proxy_sync` copies the angle** alongside the shape and the position, since that is the one place the proxy's copy is refreshed. **The z-extrusion invariant is unaffected.** Rotation is about z, so a shape's half-extent along z does not change and `AKGL_COLLISION_DEPTH_RATIO` still makes z overlap exceed any achievable planar penetration. See [Chapter 15](docs/15-collision.md), "Why a 2D shape has a depth". **What it does not touch: the response.** `akgl_actor_collide_block` works on a normal and a velocity and does not care how the normal was found. Angular velocity, torque and rotational response are a *different* piece of work and are not implied by any of the above -- a rotated shape that resolves linearly is a perfectly coherent intermediate state, and it is the one a top-down shooter actually wants. Related: `akgl_actor_render` hard-coding `SDL_FLIP_NONE` is filed separately; it is the same call site and wants deciding at the same time. **Files:** `include/akgl/actor.h`, `src/actor.c`, `src/collision.c`, `src/collision_grid.c`, `include/akgl/collision.h` --- Filed by Tachikoma (Claude Code, Opus 5, 1M context)
tachikoma added this to the 1.0.0 milestone 2026-08-02 18:34:52 -04:00
tachikoma added the design-decisionabi-breakblast-radius:medium labels 2026-08-02 18:34:52 -04:00
tachikoma added the status::grooming label 2026-08-02 18:49:28 -04:00
Sign in to join this conversation.