Rapier 2D (@dimforge/rapier2d-compat 0.20) comes in as the physics engine: the editor loads it once in the background right after opening a project from its own chunk, so the main bundle grows by 0.19 KB gzip and the first frame does not wait; loadPhysics, physicsStatus and the 100 px = 1 m conversion live in src/runtime/physics/engine.ts, __gm.physics() reports the state, unit tests pin free fall and a box resting on a box, and e2e module 41 pins that Rapier is ready from its own chunk without console noise (#145)
game-maker / gate (push) Successful in 1m46s
game-maker / suite (push) Successful in 6m12s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-18 16:56:47 +02:00
1 parent 6d7d0005cf
commit d02bcca266
9 files changed
+143 -1

No files matched your search

+2
View File
@@ -2,6 +2,7 @@ import type { Application } from 'pixi.js';
import { HttpFileStore } from '../core/store/HttpFileStore';
import { HttpProjectStore } from '../core/store/HttpProjectStore';
import type { StoredTexture, UploadedTexture } from '../core/store/TextureStore';
import { loadPhysics } from '../runtime/physics/engine';
import { requestRender, type VStack } from '../ui';
import { fontLibrary } from '../viewport/FontLibrary';
import { soundLibrary } from '../viewport/SoundLibrary';
@@ -42,5 +43,6 @@ export async function startEditor(app: Application, root: VStack): Promise<void>
fontLibrary.onChange(() => requestRender());
installDebugHook(app, state, editor);
await editor.boot();
loadPhysics().catch((error: unknown) => console.error('[physics] Rapier could not be loaded:', error));
await startPlugins(API_ENDPOINT, state, editor);
}
+2
View File
@@ -6,6 +6,7 @@ import { findAsset, findAssetOf, findObjectHolder } from '../core/model/Project'
import type { FrameRect } from '../core/model/texture';
import { findLayer } from '../core/model/tilemap';
import { effectiveScale, gameClock } from '../core/time';
import { physicsStatus } from '../runtime/physics/engine';
import {
activeDrag,
afterFrames,
@@ -641,6 +642,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
sceneSelect: () => ({ ...centreOf(editor.sceneButton), enabled: editor.sceneButton.enabled }),
/** Behaviors running without Play (`Run in editor`), and how long they have been at it. */
editorRun: () => editor.editorRunInfo(),
physics: () => physicsStatus(),
/** Rows the hierarchy shows – collapsed subtrees are not in here. */
hierarchyOrder: () => hierarchy.rowOrder(),
hierarchyChevron: (id: string) => hierarchy.chevronPosition(id),
+40
View File
@@ -0,0 +1,40 @@
import type RAPIER from '@dimforge/rapier2d-compat';
export type Rapier = typeof RAPIER;
/** Everything outside the engine is in pixels; Rapier is tuned for metres. */
export const PIXELS_PER_METRE = 100;
export const toMetres = (pixels: number): number => pixels / PIXELS_PER_METRE;
export const toPixels = (metres: number): number => metres * PIXELS_PER_METRE;
export type PhysicsStatus =
| { state: 'idle' | 'loading' }
| { state: 'ready'; ms: number; version: string }
| { state: 'failed'; error: string };
let status: PhysicsStatus = { state: 'idle' };
let loading: Promise<Rapier> | null = null;
/**
* Rapier is its own chunk with the WebAssembly inside. Every caller shares the one load, and a
* failed load stays failed rather than being tried again on each call.
*/
export function loadPhysics(): Promise<Rapier> {
if (loading) return loading;
const start = performance.now();
status = { state: 'loading' };
loading = import('@dimforge/rapier2d-compat').then(async ({ default: rapier }) => {
await rapier.init();
status = { state: 'ready', ms: Math.round(performance.now() - start), version: rapier.version() };
return rapier;
});
loading.catch((error: unknown) => {
status = { state: 'failed', error: error instanceof Error ? error.message : String(error) };
});
return loading;
}
export function physicsStatus(): PhysicsStatus {
return status;
}