Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,13 @@ export default defineConfig([
unicorn,
},
rules: {
'simple-import-sort/imports': 'error',
// Example sources are authored in TypeScript and transpiled to the linted
// `.js` by `examples:sync`; that transpile strips the blank lines between
// import groups, so a single sorted group (no group separators) is the only
// shape an example `.js` can hold. Collapse all imports into one group so
// examples that mix a package import with a relative one (e.g. a shared
// recipe) still lint clean.
'simple-import-sort/imports': ['error', { groups: [['^\\u0000', '^node:', '^@?\\w', '^', '^\\.']] }],
'simple-import-sort/exports': 'error',
'unused-imports/no-unused-imports': 'error',
'@typescript-eslint/no-empty-function': 'warn',
Expand Down
33 changes: 29 additions & 4 deletions examples/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,7 @@
"debug"
],
"capabilities": [
"pointer",
"audio"
"pointer"
]
},
{
Expand Down Expand Up @@ -1008,6 +1007,33 @@
]
}
],
"physics": [
{
"slug": "sprite-follows-body",
"path": "physics/sprite-follows-body.js",
"language": "typescript",
"title": "Sprite Follows Body",
"description": "The minimal physics binding: world.attach builds a body + collider and binds it to a sprite, which then tracks the body each step as it falls onto a static floor.",
"backend": "core",
"tags": [
"physics",
"binding"
]
},
{
"slug": "tiled-map-physics-actor",
"path": "physics/tiled-map-physics-actor.js",
"language": "typescript",
"title": "Tiled Map + Physics Actor",
"description": "Build static colliders from a tilemap object layer with the ObjectLayer-to-collider bridge, then drop a dynamic actor that falls and bounces across the rendered level.",
"backend": "core",
"tags": [
"physics",
"tilemap",
"collision"
]
}
],
"render-targets": [
{
"slug": "bloom-lite",
Expand Down Expand Up @@ -1311,8 +1337,7 @@
"scene"
],
"capabilities": [
"gamepad",
"audio"
"gamepad"
]
},
{
Expand Down
103 changes: 103 additions & 0 deletions examples/physics/sprite-follows-body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Auto-generated from sprite-follows-body.ts — edit the .ts source, not this file.
import { Application, Color, Json, Scene, Sprite, Spritesheet, Texture, Vector } from '@codexo/exojs';
import { BoxShape, PhysicsWorld } from '@codexo/exojs-physics';
import { mountControls } from '@examples/runtime';
// The minimal physics binding: `world.attach(node, { ... })` builds a body +
// collider and binds it to the node in one call. After every `world.step(...)`
// the body's position and rotation are written onto the bound sprite, so the
// sprite simply "follows the body". A static floor stops the falling actor.
const app = new Application({
canvas: {
width: 1280,
height: 720,
mount: document.body,
sizingMode: 'fit',
},
clearColor: new Color(18, 22, 33),
});
class SpriteFollowsBodyScene extends Scene {
world;
actor;
actorBody;
floor;
floorY = 0;
settled = 0;
hud;
async load(loader) {
await loader.load(Texture, {
characters: assets.demo.spritesheets.platformerCharacters.image,
pixel: assets.demo.textures.pixelWhite,
});
await loader.load(Json, { characters: assets.demo.spritesheets.platformerCharacters.data });
}
init(loader) {
const { width, height } = this.app.canvas;
// Gravity in px/s², +Y down — matches the engine's screen space.
this.world = new PhysicsWorld({ gravity: { x: 0, y: 1400 } });
const characters = new Spritesheet(loader.get(Texture, 'characters'), loader.get(Json, 'characters'));
this.floorY = height - 80;
// ── Static floor ──────────────────────────────────────────────────
// A wide static body. `world.attach` binds it to the floor sprite, so
// the sprite is positioned from the body (no manual placement needed).
const floorWidth = width - 120;
const floorHeight = 48;
this.floor = new Sprite(loader.get(Texture, 'pixel')).setAnchor(0.5);
this.floor.width = floorWidth;
this.floor.height = floorHeight;
this.floor.tint = new Color(70, 92, 120);
this.world.attach(this.floor, {
type: 'static',
position: { x: width / 2, y: this.floorY },
shape: new BoxShape(floorWidth, floorHeight),
});
// ── Dynamic actor ─────────────────────────────────────────────────
// A dynamic body dropped from above. Its collider is a box sized to the
// character art; `world.attach` binds the sprite, so it falls, lands and
// tracks the body (position + rotation) every step.
this.actor = characters.getFrameSprite('character_beige_front').setAnchor(0.5).setScale(1.1);
this.actorBody = this.world.attach(this.actor, {
type: 'dynamic',
position: { x: width / 2, y: 140 },
shape: new BoxShape(70, 90),
friction: 0.4,
restitution: 0.15,
});
// A small sideways nudge so the landing is visibly dynamic.
this.actorBody.applyImpulse(900, 0);
this.hud = mountControls({
title: 'Sprite Follows Body',
controls: [{ keys: 'Auto', action: 'actor falls and lands on the floor' }],
status: 'Dropping…',
hint: 'world.attach(sprite, { … }) creates a body + collider and binds it; world.step writes the body transform onto the sprite each frame.',
});
}
update(delta) {
// Advance the simulation; bound sprites are synced inside step().
this.world.step(delta.seconds);
const { width, height } = this.app.canvas;
const body = this.actorBody;
const restingSpeed = Math.hypot(body.linearVelocityX, body.linearVelocityY);
if (body.y > this.floorY - 60 && restingSpeed < 6) {
this.settled += delta.seconds;
}
else {
this.settled = 0;
}
this.hud.setStatus(this.settled > 0 ? `Resting on the floor (${restingSpeed.toFixed(0)} px/s)` : `Falling… y=${body.y.toFixed(0)} px`);
// Loop: after a short rest (or if it tumbles off-screen) drop it again.
if (this.settled > 1.2 || body.y > height + 200 || Math.abs(body.x - width / 2) > width) {
this.settled = 0;
body.setTransform(new Vector(width / 2, 140), (Math.random() - 0.5) * 0.6);
body.linearVelocityX = 0;
body.linearVelocityY = 0;
body.angularVelocity = 0;
body.applyImpulse((Math.random() - 0.5) * 1800, 0);
}
}
draw(context) {
context.backend.clear();
context.render(this.floor);
context.render(this.actor);
}
}
app.start(new SpriteFollowsBodyScene());
122 changes: 122 additions & 0 deletions examples/physics/sprite-follows-body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Application, Color, Json, Scene, Sprite, Spritesheet, type SpritesheetData, Texture, Vector } from '@codexo/exojs';
import { BoxShape, type PhysicsBody, PhysicsWorld } from '@codexo/exojs-physics';
import { mountControls } from '@examples/runtime';

// The minimal physics binding: `world.attach(node, { ... })` builds a body +
// collider and binds it to the node in one call. After every `world.step(...)`
// the body's position and rotation are written onto the bound sprite, so the
// sprite simply "follows the body". A static floor stops the falling actor.

const app = new Application({
canvas: {
width: 1280,
height: 720,
mount: document.body,
sizingMode: 'fit',
},
clearColor: new Color(18, 22, 33),
});

class SpriteFollowsBodyScene extends Scene {
private world!: PhysicsWorld;
private actor!: Sprite;
private actorBody!: PhysicsBody;
private floor!: Sprite;
private floorY = 0;
private settled = 0;
private hud!: ReturnType<typeof mountControls>;

override async load(loader): Promise<void> {
await loader.load(Texture, {
characters: assets.demo.spritesheets.platformerCharacters.image,
pixel: assets.demo.textures.pixelWhite,
});
await loader.load(Json, { characters: assets.demo.spritesheets.platformerCharacters.data });
}

override init(loader): void {
const { width, height } = this.app.canvas;

// Gravity in px/s², +Y down — matches the engine's screen space.
this.world = new PhysicsWorld({ gravity: { x: 0, y: 1400 } });

const characters = new Spritesheet(loader.get(Texture, 'characters'), loader.get(Json, 'characters') as SpritesheetData);

this.floorY = height - 80;

// ── Static floor ──────────────────────────────────────────────────
// A wide static body. `world.attach` binds it to the floor sprite, so
// the sprite is positioned from the body (no manual placement needed).
const floorWidth = width - 120;
const floorHeight = 48;

this.floor = new Sprite(loader.get(Texture, 'pixel')).setAnchor(0.5);
this.floor.width = floorWidth;
this.floor.height = floorHeight;
this.floor.tint = new Color(70, 92, 120);

this.world.attach(this.floor, {
type: 'static',
position: { x: width / 2, y: this.floorY },
shape: new BoxShape(floorWidth, floorHeight),
});

// ── Dynamic actor ─────────────────────────────────────────────────
// A dynamic body dropped from above. Its collider is a box sized to the
// character art; `world.attach` binds the sprite, so it falls, lands and
// tracks the body (position + rotation) every step.
this.actor = characters.getFrameSprite('character_beige_front').setAnchor(0.5).setScale(1.1);
this.actorBody = this.world.attach(this.actor, {
type: 'dynamic',
position: { x: width / 2, y: 140 },
shape: new BoxShape(70, 90),
friction: 0.4,
restitution: 0.15,
});

// A small sideways nudge so the landing is visibly dynamic.
this.actorBody.applyImpulse(900, 0);

this.hud = mountControls({
title: 'Sprite Follows Body',
controls: [{ keys: 'Auto', action: 'actor falls and lands on the floor' }],
status: 'Dropping…',
hint: 'world.attach(sprite, { … }) creates a body + collider and binds it; world.step writes the body transform onto the sprite each frame.',
});
}

override update(delta): void {
// Advance the simulation; bound sprites are synced inside step().
this.world.step(delta.seconds);

const { width, height } = this.app.canvas;
const body = this.actorBody;
const restingSpeed = Math.hypot(body.linearVelocityX, body.linearVelocityY);

if (body.y > this.floorY - 60 && restingSpeed < 6) {
this.settled += delta.seconds;
} else {
this.settled = 0;
}

this.hud.setStatus(this.settled > 0 ? `Resting on the floor (${restingSpeed.toFixed(0)} px/s)` : `Falling… y=${body.y.toFixed(0)} px`);

// Loop: after a short rest (or if it tumbles off-screen) drop it again.
if (this.settled > 1.2 || body.y > height + 200 || Math.abs(body.x - width / 2) > width) {
this.settled = 0;
body.setTransform(new Vector(width / 2, 140), (Math.random() - 0.5) * 0.6);
body.linearVelocityX = 0;
body.linearVelocityY = 0;
body.angularVelocity = 0;
body.applyImpulse((Math.random() - 0.5) * 1800, 0);
}
}

override draw(context): void {
context.backend.clear();
context.render(this.floor);
context.render(this.actor);
}
}

app.start(new SpriteFollowsBodyScene());
Loading
Loading