Colliders take a frame's outline and tiles have their own surface (#193)
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:
1 parent
3ff7e91dbf
commit
f4ec23d3d3
20 files changed
+729
-83
No files matched your search
@@ -17,6 +17,8 @@ import {
|
||||
import { findAsset, findAssetOf, type ObjectHolder, type Project } from '../core/model/Project';
|
||||
import { fontFileOf } from '../core/model/font';
|
||||
import { resolveTexture, type TextureRef } from '../core/model/texture';
|
||||
import type { FrameOutline } from '../core/model/frameOutline';
|
||||
import { colliderFrame } from '../core/properties/collider';
|
||||
import { enabledAlongChain } from '../core/properties/enabled';
|
||||
import { fontOf } from '../core/properties/fonts';
|
||||
import { resolveSceneObjectProps } from '../core/properties/resolve';
|
||||
@@ -49,6 +51,8 @@ export interface DrawItem {
|
||||
tilemap: TilemapDraw | null;
|
||||
/** How often an `Emit` node has restarted this source; a change is what makes it start over. */
|
||||
emits: number;
|
||||
/** The frame a collider of the shape "Frame collision" wears (#193), resolved here like a sprite's picture. */
|
||||
frame: FrameOutline | null;
|
||||
}
|
||||
|
||||
interface DrawList {
|
||||
@@ -167,6 +171,7 @@ export function collectHolderItems(
|
||||
particles: asset.type === 'particles' ? asset : null,
|
||||
tilemap: asset.type === 'tilemap' ? resolveTilemap(project, asset.id, props) : null,
|
||||
emits: 0,
|
||||
frame: asset.type === 'collider' ? colliderFrame(project, props) : null,
|
||||
});
|
||||
}
|
||||
const byId = new Map(items.map((i) => [i.id, i]));
|
||||
@@ -204,6 +209,7 @@ export function collectWorldItems(world: World): DrawList {
|
||||
particles: e.type === 'particles' ? (findAssetOf(world.project, e.assetId, 'particles') ?? null) : null,
|
||||
tilemap: e.type === 'tilemap' ? resolveTilemap(world.project, e.assetId, e.props) : null,
|
||||
emits: e.emits ?? 0,
|
||||
frame: e.wornFrame?.() ?? null,
|
||||
}));
|
||||
return { items, background: world.backgroundColor };
|
||||
}
|
||||
+10
-6
@@ -8,6 +8,7 @@ import {
|
||||
isTrigger,
|
||||
oneWayAngle,
|
||||
oneWayFacing,
|
||||
outlinesOf,
|
||||
} from '../core/properties/collider';
|
||||
import { SHEET_EDGE } from '../core/shader/nodes';
|
||||
import type { NineSlice } from '../core/model/texture';
|
||||
@@ -150,9 +151,10 @@ export function drawItem(
|
||||
case 'collider': {
|
||||
// Only the outline band is hit-testable, so clicks inside still reach the sprite that the
|
||||
// collider sits on.
|
||||
const shape = colliderShape(item.props, IDENTITY);
|
||||
const shape = colliderShape(item.props, IDENTITY, item.frame);
|
||||
traceShape(node, shape);
|
||||
// A polygon that crosses itself collides with nothing, and says so in red.
|
||||
// A polygon that crosses itself collides with nothing, and says so in red – as does a frame
|
||||
// outline without a frame or without a drawn outline (#193).
|
||||
const color =
|
||||
shape.kind === 'polygon' && shape.pieces.length === 0
|
||||
? Theme.colors.danger
|
||||
@@ -201,7 +203,7 @@ function shapeBounds(shape: ColliderShape): Bounds {
|
||||
{ x: p.x - shape.r, y: p.y - shape.r },
|
||||
{ x: p.x + shape.r, y: p.y + shape.r },
|
||||
])
|
||||
: shape.outline;
|
||||
: outlinesOf(shape).flat();
|
||||
return {
|
||||
left: Math.min(...points.map((p) => p.x)),
|
||||
top: Math.min(...points.map((p) => p.y)),
|
||||
@@ -280,7 +282,7 @@ function traceShape(node: Graphics, shape: ColliderShape): void {
|
||||
return;
|
||||
}
|
||||
case 'polygon':
|
||||
if (shape.outline.length >= 2) node.poly(shape.outline, true);
|
||||
for (const ring of outlinesOf(shape)) if (ring.length >= 2) node.poly(ring, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -299,10 +301,12 @@ function colliderOutlineHitArea(
|
||||
if (shape.kind === 'capsule')
|
||||
return { contains: (x, y) => Math.abs(distanceToSegment({ x, y }, shape.a, shape.b) - shape.r) <= band };
|
||||
if (shape.kind === 'polygon') {
|
||||
const ring = shape.outline;
|
||||
const rings = outlinesOf(shape);
|
||||
return {
|
||||
contains: (x, y) =>
|
||||
ring.some((a, i) => distanceToSegment({ x, y }, a, ring[(i + 1) % ring.length]!) <= band),
|
||||
rings.some((ring) =>
|
||||
ring.some((a, i) => distanceToSegment({ x, y }, a, ring[(i + 1) % ring.length]!) <= band),
|
||||
),
|
||||
};
|
||||
}
|
||||
const hw = shape.w / 2;
|
||||
|
||||
Reference in new issue
Block a user