One-way platforms, raycast and collision stay with contact data: a collider can be one-way with a side it blocks from (turning with its object, drawn thick in the scene view) and a tile can be one-way from above, one-way cells never merge with solid tiles, a contact blocks only when its normal is within 45 degrees of the blocking side, the new node Ignore one-way lets an object drop through for a while, and it all holds for Move and for dynamic bodies in Rapier, where the decision is made before each step because the hook may not touch the engine (#155); the new Raycast node answers hit, object, collider, point, normal and distance in our own f64 geometry, the same in editor run and play, and the editor run draws each ray for 0.25 s (#153); On collision stay gains an interval with its own clock per pair, and stay and enter gain the contact point next to normal and impulse (#149); fixes that every Rapier collider of a tilemap had the shape of its last cell
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
947a6b1977
commit
6a31f5e0ad
33 files changed
+2064
-125
No files matched your search
@@ -208,7 +208,10 @@ export class Editor implements PlayHost {
|
||||
// The game view is the one place where no key means anything to the editor, so that is where
|
||||
// a behavior may have them – unless a widget in it is focused and wants them itself.
|
||||
focused: () => this.tab === 'game' && ui().focus.focused === null,
|
||||
onWorld: (world) => this.gameView.setEditorWorld(world),
|
||||
onWorld: (world) => {
|
||||
this.gameView.setEditorWorld(world);
|
||||
this.viewport.setEditorWorld(world);
|
||||
},
|
||||
});
|
||||
this.projects = new ProjectSession(state, store, () => {
|
||||
if (this.play.isPlaying) this.play.stop();
|
||||
|
||||
@@ -313,6 +313,8 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
gizmos: () => viewport.gizmoInfo(),
|
||||
/** The corner handles of the polygon collider being edited, in global pixels (#151). */
|
||||
polygonCorners: () => viewport.cornerInfo(),
|
||||
/** The rays the editor run cast in the last moment, as the Scene tab draws them, in world units (#153). */
|
||||
rays: () => viewport.rayInfo(),
|
||||
layerOf: (id: string) => viewport.layerOf(id),
|
||||
/** The icon a particle source wears in the Scene tab, with its name and its owner link. */
|
||||
sceneIcon: (id: string) => viewport.iconInfo(id),
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
tilemapRows,
|
||||
tilemapTileSize,
|
||||
} from '../../core/model/tilemap';
|
||||
import type { CellRect } from '../../core/model/tilemapCollision';
|
||||
import type { CellPolygon, CellRect } from '../../core/model/tilemapCollision';
|
||||
import { footprintCorners } from '../../core/properties/bounds';
|
||||
import { colliderPoints, COLLIDER_POINTS_KEY } from '../../core/properties/collider';
|
||||
import { localMatrixOf, parentTransform, transformOf } from '../../core/properties/transform';
|
||||
@@ -60,8 +60,9 @@ import {
|
||||
import { gripAt, withCornerAfter, withCornerMoved, withoutCorner } from '../polygonEdit';
|
||||
import { collectHolderItems, type DrawItem } from '../../viewport/items';
|
||||
import type { LayerFocus } from '../../viewport/tilemap';
|
||||
import { dashedLine, drawSourceIcon, placeNode } from '../../viewport/symbols';
|
||||
import { dashedLine, drawSourceIcon, markBlockingSide, placeNode } from '../../viewport/symbols';
|
||||
import { screenToWorld, type View, worldToScreen } from '../../viewport/view';
|
||||
import { type CastRay, RAY_SHOWN, type World } from '../../runtime/World';
|
||||
import { FreeViewport } from './FreeViewport';
|
||||
import type { Frame } from './WorldViewport';
|
||||
|
||||
@@ -161,6 +162,8 @@ export class SceneViewport extends FreeViewport {
|
||||
private readonly icons = new Map<string, Graphics>();
|
||||
/** Where each placed tilemap blocks, outlined like a collider (#132). */
|
||||
private readonly solids = new Map<string, Graphics>();
|
||||
private editorWorld: World | null = null;
|
||||
private lastRays: CastRay[] = [];
|
||||
private dragging: ObjectDrag | null = null;
|
||||
private corner: CornerDrag | null = null;
|
||||
/** The corners drawn for the edited polygon, in global pixels (test hook). */
|
||||
@@ -709,6 +712,31 @@ export class SceneViewport extends FreeViewport {
|
||||
protected override afterItems(items: DrawItem[], view: View): void {
|
||||
this.drawParentLinks(items, view.scale);
|
||||
this.drawGizmos(items, view);
|
||||
this.drawRays(view);
|
||||
}
|
||||
|
||||
/** The editor run's world, whose rays are drawn over the scene (#153); null when nothing runs. */
|
||||
setEditorWorld(world: World | null): void {
|
||||
this.editorWorld = world;
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
/** The rays drawn right now, in world units (test hook). */
|
||||
rayInfo(): { from: Vec2; to: Vec2; hit: boolean }[] {
|
||||
return this.lastRays.map(({ from, to, hit }) => ({ from: { ...from }, to: { ...to }, hit }));
|
||||
}
|
||||
|
||||
/** The rays the editor run cast in the last moment: to what they met, or as far as they reached. */
|
||||
private drawRays(view: View): void {
|
||||
const world = this.editorWorld;
|
||||
this.lastRays = world ? world.rays.filter((ray) => ray.time >= world.time - RAY_SHOWN) : [];
|
||||
for (const { from, to, hit } of this.lastRays) {
|
||||
const color = hit ? Theme.colors.collider : Theme.colors.textMuted;
|
||||
const a = worldToScreen(view, from.x, from.y);
|
||||
const b = worldToScreen(view, to.x, to.y);
|
||||
this.gizmos.moveTo(a.x, a.y).lineTo(b.x, b.y).stroke({ width: 1.5, color });
|
||||
if (hit) this.gizmos.circle(b.x, b.y, 3).fill(color);
|
||||
}
|
||||
}
|
||||
|
||||
protected override syncDecorations(item: DrawItem, onScreen: boolean, scale: number, extent: Extent): void {
|
||||
@@ -727,7 +755,13 @@ export class SceneViewport extends FreeViewport {
|
||||
}
|
||||
|
||||
/** The blocking rectangles and outlined cells of a placed tilemap as outlined, in cells (test hook). */
|
||||
solidInfo(id: string): { visible: boolean; rects: CellRect[]; outlines: Vec2[][] } | null {
|
||||
solidInfo(id: string): {
|
||||
visible: boolean;
|
||||
rects: CellRect[];
|
||||
outlines: Vec2[][];
|
||||
oneWay: CellRect[];
|
||||
oneWayOutlines: Vec2[][];
|
||||
} | null {
|
||||
const node = this.solids.get(id);
|
||||
const item = this.lastItems.find((i) => i.id === id);
|
||||
if (!node || !item?.tilemap) return null;
|
||||
@@ -735,6 +769,8 @@ export class SceneViewport extends FreeViewport {
|
||||
visible: node.visible,
|
||||
rects: item.tilemap.solids.map((rect) => ({ ...rect })),
|
||||
outlines: item.tilemap.outlines.map((polygon) => polygon.outline.map((p) => ({ ...p }))),
|
||||
oneWay: item.tilemap.oneWay.solids.map((rect) => ({ ...rect })),
|
||||
oneWayOutlines: item.tilemap.oneWay.outlines.map((polygon) => polygon.outline.map((p) => ({ ...p }))),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -747,7 +783,8 @@ export class SceneViewport extends FreeViewport {
|
||||
let node = this.solids.get(item.id);
|
||||
const rects = item.tilemap?.solids ?? [];
|
||||
const outlines = item.tilemap?.outlines ?? [];
|
||||
if (!show || rects.length + outlines.length === 0) {
|
||||
const oneWay = item.tilemap?.oneWay ?? { solids: [], outlines: [] };
|
||||
if (!show || rects.length + outlines.length + oneWay.solids.length + oneWay.outlines.length === 0) {
|
||||
if (node) node.visible = false;
|
||||
return;
|
||||
}
|
||||
@@ -762,16 +799,34 @@ export class SceneViewport extends FreeViewport {
|
||||
const tile = tilemapTileSize(item.props);
|
||||
const width = 2 / (scale * scaleOf(item.m));
|
||||
node.clear();
|
||||
for (const rect of rects) {
|
||||
const corner = cellOrigin(item.props, rect.x, rect.y);
|
||||
node.rect(corner.x, corner.y, rect.w * tile, rect.h * tile);
|
||||
}
|
||||
for (const polygon of outlines)
|
||||
node.poly(
|
||||
polygon.outline.map((p) => cellOrigin(item.props, p.x, p.y)),
|
||||
true,
|
||||
const color = Theme.colors.collider;
|
||||
const trace = (rects: readonly CellRect[], outlines: readonly CellPolygon[]) => {
|
||||
for (const rect of rects) {
|
||||
const corner = cellOrigin(item.props, rect.x, rect.y);
|
||||
node.rect(corner.x, corner.y, rect.w * tile, rect.h * tile);
|
||||
}
|
||||
for (const polygon of outlines)
|
||||
node.poly(
|
||||
polygon.outline.map((p) => cellOrigin(item.props, p.x, p.y)),
|
||||
true,
|
||||
);
|
||||
};
|
||||
trace(rects, outlines);
|
||||
node.fill({ color, alpha: 0.12 }).stroke({ width, color });
|
||||
trace(oneWay.solids, oneWay.outlines);
|
||||
node.fill({ color, alpha: 0.06 }).stroke({ width: width / 2, color });
|
||||
// A platform's top is what blocks, so that is what stands out.
|
||||
for (const rect of oneWay.solids) {
|
||||
const a = cellOrigin(item.props, rect.x, rect.y);
|
||||
const b = cellOrigin(item.props, rect.x + rect.w, rect.y + rect.h);
|
||||
markBlockingSide(
|
||||
node,
|
||||
{ left: a.x, top: a.y, right: b.x, bottom: b.y },
|
||||
{ x: 0, y: -1 },
|
||||
width * 2,
|
||||
color,
|
||||
);
|
||||
node.fill({ color: Theme.colors.collider, alpha: 0.12 }).stroke({ width, color: Theme.colors.collider });
|
||||
}
|
||||
}
|
||||
|
||||
/** The icon of a particle source, the name beside it and whether it hangs off its owner (test hook). */
|
||||
|
||||
Reference in new issue
Block a user