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
@@ -12,6 +12,7 @@ import type { Vec2 } from '../../core/model/Properties';
|
||||
import { signedArea } from '../../core/model/polygon';
|
||||
import { parentChain } from '../../core/model/walk';
|
||||
import { collisionFilterOf, interactionGroups } from '../../core/model/collisionLayers';
|
||||
import type { TileMaterial } from '../../core/model/tilemapCollision';
|
||||
import {
|
||||
type ColliderShape,
|
||||
contactBetween,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
shapeOf,
|
||||
type SolidShape,
|
||||
Solids,
|
||||
surfaceOf,
|
||||
} from '../systems/solids';
|
||||
import type { System } from '../systems/System';
|
||||
import { type Rapier, toMetres, toPixels } from './engine';
|
||||
@@ -507,10 +509,11 @@ export class PhysicsSystem implements System, CollisionSource {
|
||||
const local = shapes.map((solid) => ({ ...solid, pieces: piecesInBody(origin, solid.shape) }));
|
||||
const shapeKey =
|
||||
JSON.stringify(
|
||||
local.map(({ entity, pieces, facing }) => [
|
||||
local.map(({ entity, pieces, facing, surface }) => [
|
||||
entity.id,
|
||||
pieces.map(pieceKey),
|
||||
facing && [round(facing.x), round(facing.y), facing.cone],
|
||||
surface ?? 0,
|
||||
entity.type === 'collider' ? [isTrigger(entity.props), colliderMaterial(entity.props)] : 0,
|
||||
// The layers sit on the engine collider, so changing one has to rebuild it (#152).
|
||||
interactionGroups(collisionFilterOf(entity.props)),
|
||||
@@ -530,10 +533,10 @@ export class PhysicsSystem implements System, CollisionSource {
|
||||
(sum, { entity, shape }) => sum + (isSolid(entity) ? shapeArea(shape) : 0),
|
||||
0,
|
||||
);
|
||||
for (const { entity, shape, facing, pieces } of local)
|
||||
for (const { entity, shape, facing, surface, pieces } of local)
|
||||
for (const piece of pieces) {
|
||||
const share = mass > 0 && area > 0 ? (mass * pieceArea(piece)) / area : 0;
|
||||
const desc = this.colliderDesc(entity, piece, share, facing !== null);
|
||||
const desc = this.colliderDesc(entity, piece, share, facing !== null, surface);
|
||||
if (!desc) continue;
|
||||
const collider = this.engine!.createCollider(desc, body.body);
|
||||
body.colliders.push(collider);
|
||||
@@ -574,7 +577,13 @@ export class PhysicsSystem implements System, CollisionSource {
|
||||
}
|
||||
|
||||
/** The engine collider of one piece; null for a piece of no size, which the engine would refuse. */
|
||||
private colliderDesc(entity: Entity, piece: Piece, mass: number, oneWay: boolean): ColliderDesc | null {
|
||||
private colliderDesc(
|
||||
entity: Entity,
|
||||
piece: Piece,
|
||||
mass: number,
|
||||
oneWay: boolean,
|
||||
surface?: TileMaterial,
|
||||
): ColliderDesc | null {
|
||||
const R = this.rapier;
|
||||
if (pieceArea(piece) <= 0) return null;
|
||||
let desc: ColliderDesc | null;
|
||||
@@ -597,9 +606,10 @@ export class PhysicsSystem implements System, CollisionSource {
|
||||
if (!desc) return null;
|
||||
if (piece.kind !== 'convex')
|
||||
desc.setTranslation(toMetres(piece.x), toMetres(piece.y)).setRotation(piece.angle);
|
||||
// A tile has no material of its own; it gets what a new collider starts with.
|
||||
// A tile's cell brings its tile's surface (#193); a density it does not need, a map never moves.
|
||||
const material = colliderMaterial(entity.type === 'collider' ? entity.props : {});
|
||||
desc.setFriction(material.friction).setRestitution(material.bounciness);
|
||||
const { friction, bounciness } = surfaceOf(entity, surface);
|
||||
desc.setFriction(friction).setRestitution(bounciness);
|
||||
// The livelier side wins, as in Box2D: a ball that should bounce does so on plain ground too.
|
||||
desc.setRestitutionCombineRule(R.CoefficientCombineRule.Max);
|
||||
if (!isSolid(entity)) desc.setSensor(true).setDensity(0);
|
||||
|
||||
Reference in new issue
Block a user