A project with three tilemap layers, two of them with collision on
water and hill tiles, ran at 33 to 52 fps. A CPU profile showed that
almost all of the time went into the collision geometry of the map.
It was rebuilt from scratch every frame although nothing had changed.
The animated water makes the views redraw every frame, so the cost
was never skipped.
- New module tilemapMemo keeps what is derived from the cells: the
tiles of a layer, the blocking cells, the rectangles and polygons.
It is reused as long as the content stays the same.
- A brush stroke paints into the existing cells object. The memo
therefore compares the content cell by cell, not the identity.
The comparison walks two arrays in insertion order.
- Clipped outlines are kept per outline list. The model always
replaces that list instead of changing it.
- PhysicsSystem no longer rebuilds the shapes of a tilemap each tick
while its cells, placement and settings are unchanged. Before, it
also built a JSON key from all world shapes every tick.
- The overlaid layers after Set tile in a run stay stable. Otherwise
the memo would miss on every call after the first edit.
The #184 work only helped Move and raycasts, not this path.
Measured on stein2 (i5-7500T, HD 630), script time per frame:
- Scene, nothing selected: 17.7 ms, 52 fps -> 5.8 ms, 60 fps
- Tilemap selected: 26.2 ms, 38 fps -> 8.6 ms, 60 fps
- Play: 29.9 ms, 33 fps -> 8.7 ms, 60 fps
About 3 ms per frame remain. The content comparison costs roughly
60 us per 700-cell layer and runs about 40 times a frame. The mesh
geometries in TilemapField are still rebuilt every frame.
Tests:
- ./check.sh: 162 files, 2377 tests, CHECK EXIT 0 (before 2370)
- e2e 1-5 and 39-43: ALL PASSED, 289 checks
- 7 new unit tests in tests/core/tilemap-memo.test.ts
- 5 counter checks (content ignored, no memo, outline by kind only,
physics always rebuilds, layer settings ignored) each fail their
test. All originals are back, checked with cmp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a sprite with a dynamic rigidbody in the scene view during
play barely moved it. Only a quick drag and release got through. A
dragged object should follow the mouse as if it had no rigidbody and
start from rest when it is let go.
- The scene view reports the dragged objects as held through
EditorState. The game tick no longer writes its own position over a
drag position that has not been read in yet. This affected every
object in play, not only rigidbodies.
- A held dynamic body turns kinematic and is set straight to the drag
target. Gravity and velocity no longer pull it away between two
mouse moves, and the "moved directly" warning stays quiet.
- A body counts as held when the object itself, an object above it
or its rigidbody is dragged.
- On release the body turns dynamic again with linear and angular
velocity set to 0.
Not changed: a press on an unselected crate still lands on its
same-sized collider (rule from #203). Dragging while play is paused
and in the editor run is untouched.
Tests:
- ./check.sh: 155 files, 2273 tests, CHECK EXIT 0 (before 2271).
- e2e 41 alone: 21 checks. e2e 1-16,39-43: 641 checks, all passed.
- 2 unit tests: a held body stays where the drag puts it and starts
from rest, also for a grip shorter than one physics step. The tick
does not overwrite a drag position that is not read in yet.
- 1 e2e check in 41b: the crate is lifted 150 units with the real
mouse, holds exactly at -3490 for a second, then falls from rest
(1.3 px, then 5.1 px) with an empty log.
- Counter-checks: without holding in physics the crate falls back to
the ground and the old warning shows. With the tick writing back
the unit test fails. Without zeroing, a short grip keeps 490 px/s
and the test fails.
- Check total 1356 + 1 = 1357, calculated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PhysicsSystem had 940 lines. The step logic shares its state with
the engine handles of parts and bodies, so a separate class would
need almost the whole system as its interface. That part stays. Two
pieces without own state move out instead. Behaviour is unchanged.
- passesOneWay in oneWay.ts is the one-way rule as a pure function.
It can now be tested without Rapier.
- bodyGeometry.ts holds the collider shapes and the pose maths.
- PhysicsSystem is down to 820 lines.
Tests:
- ./check.sh: 146 files, 2199 tests, CHECK EXIT 0
- 5 new unit tests for the one-way rule without an engine.
- New tests/core/body-geometry.test.ts: a capsule turned by 90
degrees went unnoticed before. Both tests fail against that bend.
- Counter-checks on landing hold and teleport detection fail in
3 to 6 tests each.
- e2e 1-28,31,36,39-43 on the final state: ALL PASSED, 1060 checks
- Each of the four #215 commits was checked green on its own in a
scratch worktree: 2190, 2192, 2192, 2199 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>