Colliders take a frame's outline and tiles have their own surface (#193)
game-maker / gate (push) Successful in 2m11s
game-maker / suite (push) Successful in 7m33s

Issue #193 asked where friction, bounciness and density of a tile
live. Tiles had a fixed surface (friction 0.5, bounciness 0) and a
collider could not use the outline drawn on a texture frame (#141).
Both gaps are closed here.

- A collider has a fifth shape "Frame collision" with a Frame row.
  The frame is picked like a sprite's texture and stored as the
  value 'frame'.
- The shape is linked, not copied. Redrawing the outline in the
  texture tab changes every collider using that frame, in the editor
  and on the next play.
- The frame centre sits on the collider origin, one image pixel is
  one unit. The shape turns, scales and mirrors like any other.
- Every outline ring blocks. Each ring is split into convex parts the
  same way a polygon is.
- Without a frame, with a whole texture or with a frame that has no
  outline, the collider is drawn red as a box and blocks nothing.
- Move, Rapier, raycast, detection, hit test, drawing and footprint
  all resolve the frame. At runtime an entity looks the frame up
  live in the current project.
- A tile has Friction and Bounciness next to Solid and One-way. The
  rows only show when Solid is on. Defaults and mixing rules match
  the collider. Density is left out because a tilemap never moves.
- Tilemap cells are grouped by surface as well as by one-way angle.
  Ice and stone never merge into one rectangle, and every cell brings
  its surface to Move and Rapier.

Findings:

- A tile with a collision scale below 1 is no longer flush with a
  neighbour of another surface. It is inset there like at an open
  side, the same as next to one-way tiles today.
- A bounce test first measured 0.84. The ball had landed on the hero
  at the origin, not on the tile. On the tile it measures 0.993.

Tests:

- ./check.sh: 130 files, 1931 tests, exit 0 (was 129 / 1917).
- 14 new unit tests in tests/core/surface.test.ts. The Move tests run
  against the editor run and the physics system.
- 3 new e2e checks: 40l (surface rows follow Solid), 42e (collider
  has frame size, a walker climbs the slope in the frame).
- GM_STEPS=1-8,39-43 freshly seeded: ALL PASSED, 376 checks.
- Three counter-probes bite: Move ignores the tile surface (2 fail),
  Rapier ignores it (1 fails), frame shape falls back to a box
  (5 fail).
- Check sum 1198 is computed (1195 + 3), the full run belongs to CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-21 07:29:26 +02:00
1 parent 3ff7e91dbf
commit f4ec23d3d3
20 files changed
+729 -83

No files matched your search

+29 -2
View File
@@ -22,10 +22,12 @@ import {
import { findAssetOf, type Project } from '../model/Project';
import { ALL_LAYERS, COLLIDES_WITH_KEY, COLLISION_LAYER_KEY } from '../model/collisionLayers';
import {
TILE_BOUNCINESS_KEY,
TILE_COLLISION_SCALE_KEY,
TILE_COLLISION_SCALE_MAX,
TILE_COLLISION_SCALE_MIN,
TILE_FPS_KEY,
TILE_FRICTION_KEY,
TILE_KIND_KEY,
TILE_ONE_WAY_ANGLE_KEY,
TILE_ONE_WAY_KEY,
@@ -105,6 +107,25 @@ const ONE_WAY_ANGLE_ROW: FlatDef<'number'> = {
shownWith: 'oneWay',
};
/** The surface a collider or a solid tile (#193) offers, read by the physics engine and by `Move` (#146, #150). */
const FRICTION_ROW: FlatDef<'number'> = {
key: 'friction',
label: 'Friction',
kind: 'number',
default: 0.5,
min: 0,
step: 0.05,
};
const BOUNCINESS_ROW: FlatDef<'number'> = {
key: 'bounciness',
label: 'Bounciness',
kind: 'number',
default: 0,
min: 0,
max: 1,
step: 0.05,
};
/**
* Which colliders may meet (#152). Both rows sit on colliders and on tilemaps, so a map can be
* ground that only some things walk on. The defaults are layer 0 against every layer, which is
@@ -343,6 +364,9 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
// A solid tile that only blocks from above, like a platform one jumps through (#155).
{ key: TILE_ONE_WAY_KEY, label: 'One-way (from above)', kind: 'boolean', default: false },
{ ...ONE_WAY_ANGLE_ROW, key: TILE_ONE_WAY_ANGLE_KEY, shownWith: TILE_ONE_WAY_KEY },
// No density: a map never moves, so what its cells would weigh counts for nothing.
{ ...FRICTION_ROW, key: TILE_FRICTION_KEY, shownWith: TILE_SOLID_KEY },
{ ...BOUNCINESS_ROW, key: TILE_BOUNCINESS_KEY, shownWith: TILE_SOLID_KEY },
// Below 1 a cell blocks less than its square, above 1 more – but only on the sides without a
// blocking neighbour, so a wall of such cells stays closed (#140).
{
@@ -433,6 +457,7 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
{ value: 'circle', label: 'Circle' },
{ value: 'capsule', label: 'Capsule' },
{ value: 'polygon', label: 'Polygon' },
{ value: 'frame', label: 'Frame collision' },
],
},
// A box and a capsule fill width × height; the capsule is rounded along the longer side.
@@ -450,10 +475,12 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
{ x: -50, y: 50 },
],
},
// The frame whose collision outlines (#141) the "Frame collision" shape wears, kept linked (#193).
{ key: 'frame', label: 'Frame', kind: 'texture', default: '' },
{ key: 'isTrigger', label: 'Trigger (no blocking)', kind: 'boolean', default: false },
// The material, read by the physics engine where a rigidbody takes part (#146).
{ key: 'friction', label: 'Friction', kind: 'number', default: 0.5, min: 0, step: 0.05 },
{ key: 'bounciness', label: 'Bounciness', kind: 'number', default: 0, min: 0, max: 1, step: 0.05 },
FRICTION_ROW,
BOUNCINESS_ROW,
{ key: 'density', label: 'Density', kind: 'number', default: 1, min: 0, step: 0.1 },
// Blocks only what comes from that side, turned with the object; the rest passes through (#155).
{ key: 'oneWay', label: 'One-way', kind: 'boolean', default: false },