Add shared-scene 2D authoring, sprite animation and Rapier2D physics
CI / validate (push) Canceled after 0s
CI / validate (push) Canceled after 0s
This commit is contained in:
+295
-44
@@ -14,9 +14,14 @@ import { workerSource } from "./script-host.ts";
|
||||
import { inspectModel } from "./model.ts";
|
||||
import { CharacterMotor } from "./character.ts";
|
||||
import { bindViewInput } from "./view-input.ts";
|
||||
import { Graphics2D } from "./graphics2d.ts";
|
||||
import { Physics2D } from "./physics2d.ts";
|
||||
import { View2D } from "./view2d.ts";
|
||||
import { validate2D, type TileCell } from "./two-d.ts";
|
||||
import { RuntimePresentation } from "./presentation.ts";
|
||||
export interface RuntimeCallbacks {
|
||||
select?: (id: string | null) => void;
|
||||
paintTiles?: (id: string, cells: TileCell[]) => void;
|
||||
transform?: (id: string, t: Entity["transform"]) => void;
|
||||
log?: (level: string, message: string, id?: string) => void;
|
||||
stats?: (s: any) => void;
|
||||
@@ -31,6 +36,9 @@ export interface RuntimeOptions {
|
||||
}
|
||||
let physicsModule: Promise<any> | null = null;
|
||||
export class FormaRuntime {
|
||||
graphics2d!: Graphics2D;
|
||||
physics2d?: Physics2D;
|
||||
view2d: View2D;
|
||||
engine: B.Engine;
|
||||
scene!: B.Scene;
|
||||
camera!: B.ArcRotateCamera;
|
||||
@@ -123,12 +131,18 @@ export class FormaRuntime {
|
||||
false,
|
||||
);
|
||||
if (!options.headless) {
|
||||
this.cleanup.push(bindViewInput(canvas, {
|
||||
get playing() { return runtime.playing; },
|
||||
get paused() { return runtime.paused; },
|
||||
firstPerson: () => this.firstPerson(),
|
||||
lookBy: (x, y) => this.lookBy(x, y),
|
||||
}));
|
||||
this.cleanup.push(
|
||||
bindViewInput(canvas, {
|
||||
get playing() {
|
||||
return runtime.playing;
|
||||
},
|
||||
get paused() {
|
||||
return runtime.paused;
|
||||
},
|
||||
firstPerson: () => this.firstPerson(),
|
||||
lookBy: (x, y) => this.lookBy(x, y),
|
||||
}),
|
||||
);
|
||||
const resize = new ResizeObserver(() => this.engine.resize());
|
||||
resize.observe(canvas);
|
||||
this.cleanup.push(() => resize.disconnect());
|
||||
@@ -204,6 +218,20 @@ export class FormaRuntime {
|
||||
B.Matrix.Identity(),
|
||||
this.scene.activeCamera,
|
||||
);
|
||||
if (
|
||||
(this.state.find((n) => n.enabled && n.components.camera)
|
||||
?.components.camera?.mode || activeScene(this.document).mode) ===
|
||||
"2d"
|
||||
) {
|
||||
if (Math.abs(ray.direction.z) > 1e-6) {
|
||||
const t = -ray.origin.z / ray.direction.z;
|
||||
this.input.aim = ray.origin
|
||||
.add(ray.direction.scale(t))
|
||||
.asArray() as Vec3;
|
||||
this.input.pointer = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Math.abs(ray.direction.y) > 1e-6) {
|
||||
const t = -ray.origin.y / ray.direction.y;
|
||||
if (t > 0) {
|
||||
@@ -225,6 +253,7 @@ export class FormaRuntime {
|
||||
window.removeEventListener("pointerup", pu);
|
||||
});
|
||||
}
|
||||
this.view2d = new View2D(this, canvas);
|
||||
this.engine.runRenderLoop(() => this.frame());
|
||||
}
|
||||
private firstPerson() {
|
||||
@@ -241,7 +270,8 @@ export class FormaRuntime {
|
||||
this.actionQueue.add(name);
|
||||
}
|
||||
releaseInput() {
|
||||
if (!this.options.headless && document.pointerLockElement === this.canvas) document.exitPointerLock();
|
||||
if (!this.options.headless && document.pointerLockElement === this.canvas)
|
||||
document.exitPointerLock();
|
||||
this.keys.clear();
|
||||
this.actionQueue.clear();
|
||||
this.automation = null;
|
||||
@@ -252,20 +282,24 @@ export class FormaRuntime {
|
||||
const handles = new Map(
|
||||
[...this.colliders].map(([id, col]) => [col.handle, id]),
|
||||
);
|
||||
return Object.fromEntries(
|
||||
[...this.motors].map(([id, m]) => [
|
||||
id,
|
||||
{
|
||||
grounded: m.grounded,
|
||||
velocity: { ...m.velocity },
|
||||
actualVelocity: { ...m.actualVelocity },
|
||||
contacts: m.contacts.map((c) => ({
|
||||
entityId: handles.get(c.handle),
|
||||
normal: c.normal,
|
||||
})),
|
||||
},
|
||||
]),
|
||||
);
|
||||
return {
|
||||
...this.physics2d?.snapshot(),
|
||||
...Object.fromEntries(
|
||||
[...this.motors].map(([id, m]) => [
|
||||
id,
|
||||
{
|
||||
dimension: 3,
|
||||
grounded: m.grounded,
|
||||
velocity: { ...m.velocity },
|
||||
actualVelocity: { ...m.actualVelocity },
|
||||
contacts: m.contacts.map((c) => ({
|
||||
entityId: handles.get(c.handle),
|
||||
normal: c.normal,
|
||||
})),
|
||||
},
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
private enqueue<T>(fn: () => Promise<T>) {
|
||||
const p = this.tasks.then(() => {
|
||||
@@ -295,6 +329,7 @@ export class FormaRuntime {
|
||||
target: this.camera.target.clone(),
|
||||
}
|
||||
: null;
|
||||
this.graphics2d?.dispose();
|
||||
this.scene?.dispose();
|
||||
this.nodes.clear();
|
||||
this.animations.clear();
|
||||
@@ -305,6 +340,9 @@ export class FormaRuntime {
|
||||
this.flashes.clear();
|
||||
const scene = (this.scene = new B.Scene(this.engine));
|
||||
scene.useRightHandedSystem = true;
|
||||
this.graphics2d = new Graphics2D(scene, (name, data, id) =>
|
||||
this.callbacks.event?.(name, data, id),
|
||||
);
|
||||
scene.clearColor = B.Color4.FromHexString(p.settings.background + "ff");
|
||||
this.camera = new B.ArcRotateCamera(
|
||||
"Editor",
|
||||
@@ -340,7 +378,8 @@ export class FormaRuntime {
|
||||
}
|
||||
scene.activeCamera = this.camera;
|
||||
const sky = new B.HemisphericLight("Sky", B.Vector3.Up(), scene);
|
||||
sky.intensity = p.settings.rendering?.defaultLights === false ? 0 : p.settings.ambient;
|
||||
sky.intensity =
|
||||
p.settings.rendering?.defaultLights === false ? 0 : p.settings.ambient;
|
||||
sky.groundColor = B.Color3.FromHexString("#8a8475");
|
||||
const sun = new B.DirectionalLight(
|
||||
"Sun",
|
||||
@@ -380,6 +419,7 @@ export class FormaRuntime {
|
||||
scene.onPointerObservable.add((info) => {
|
||||
if (
|
||||
!this.playing &&
|
||||
!this.view2d.brush &&
|
||||
info.type === B.PointerEventTypes.POINTERTAP &&
|
||||
info.event.button === 0
|
||||
)
|
||||
@@ -413,9 +453,32 @@ export class FormaRuntime {
|
||||
});
|
||||
});
|
||||
}
|
||||
if (this.view2d?.enabled) {
|
||||
const g = this.gizmos.gizmos;
|
||||
if (g.positionGizmo) {
|
||||
g.positionGizmo.zGizmo.isEnabled = false;
|
||||
g.positionGizmo.xGizmo.isEnabled = true;
|
||||
g.positionGizmo.yGizmo.isEnabled = true;
|
||||
}
|
||||
if (g.rotationGizmo) {
|
||||
g.rotationGizmo.xGizmo.isEnabled = false;
|
||||
g.rotationGizmo.yGizmo.isEnabled = false;
|
||||
g.rotationGizmo.zGizmo.isEnabled = true;
|
||||
}
|
||||
if (g.scaleGizmo) g.scaleGizmo.zGizmo.isEnabled = false;
|
||||
} else {
|
||||
const g = this.gizmos.gizmos;
|
||||
if (g.positionGizmo) g.positionGizmo.zGizmo.isEnabled = true;
|
||||
if (g.rotationGizmo) {
|
||||
g.rotationGizmo.xGizmo.isEnabled = true;
|
||||
g.rotationGizmo.yGizmo.isEnabled = true;
|
||||
}
|
||||
if (g.scaleGizmo) g.scaleGizmo.zGizmo.isEnabled = true;
|
||||
}
|
||||
this.select(this.selection);
|
||||
}
|
||||
setSnap(enabled: boolean) {
|
||||
this.view2d.snap = enabled ? 0.5 : 0;
|
||||
const g = this.gizmos.gizmos;
|
||||
if (g.positionGizmo) g.positionGizmo.snapDistance = enabled ? 0.5 : 0;
|
||||
if (g.rotationGizmo)
|
||||
@@ -426,25 +489,53 @@ export class FormaRuntime {
|
||||
this.selection = id;
|
||||
if (!this.highlight) return;
|
||||
this.highlight.removeAllMeshes();
|
||||
this.graphics2d?.outline(
|
||||
this.playing ? undefined : this.state.find((n) => n.id === id),
|
||||
this.nodes.get(id || ""),
|
||||
);
|
||||
const node = this.nodes.get(id || "");
|
||||
if (node && !this.playing)
|
||||
for (const mesh of node.getChildMeshes())
|
||||
if (mesh instanceof B.Mesh)
|
||||
if (
|
||||
mesh instanceof B.Mesh &&
|
||||
!this.graphics2d.visuals.has(mesh.metadata?.entityId)
|
||||
)
|
||||
this.highlight.addMesh(mesh, B.Color3.FromHexString("#d59565"));
|
||||
this.gizmos.attachToNode(this.playing ? null : node || null);
|
||||
}
|
||||
setView2D(enabled: boolean) {
|
||||
this.view2d.set(enabled);
|
||||
}
|
||||
focus(id?: string) {
|
||||
const n = this.nodes.get(id || this.selection || "");
|
||||
if (n && n.getChildMeshes().length) {
|
||||
const b = n.getHierarchyBoundingVectors(true);
|
||||
this.camera.setTarget(b.min.add(b.max).scale(0.5));
|
||||
this.camera.radius = Math.max(4, B.Vector3.Distance(b.min, b.max) * 1.7);
|
||||
if (this.view2d.enabled) {
|
||||
this.view2d.width = Math.max(
|
||||
2,
|
||||
Math.max(
|
||||
b.max.x - b.min.x,
|
||||
((b.max.y - b.min.y) * this.engine.getRenderWidth()) /
|
||||
this.engine.getRenderHeight(),
|
||||
) * 1.4,
|
||||
);
|
||||
this.view2d.update();
|
||||
} else
|
||||
this.camera.radius = Math.max(
|
||||
4,
|
||||
B.Vector3.Distance(b.min, b.max) * 1.7,
|
||||
);
|
||||
} else {
|
||||
this.camera.setTarget(n?.getAbsolutePosition() || B.Vector3.Zero());
|
||||
this.camera.radius = n ? 6 : 31;
|
||||
}
|
||||
}
|
||||
topView() {
|
||||
if (this.view2d.enabled) {
|
||||
this.view2d.set(true);
|
||||
return;
|
||||
}
|
||||
this.camera.alpha = -Math.PI / 2;
|
||||
this.camera.beta = 0.015;
|
||||
this.camera.radius = 26;
|
||||
@@ -458,9 +549,12 @@ export class FormaRuntime {
|
||||
root.scaling.copyFromFloats(...n.transform.scale);
|
||||
root.setEnabled(n.enabled);
|
||||
root.computeWorldMatrix(true);
|
||||
this.graphics2d?.update(n, true);
|
||||
const c = n.components.material;
|
||||
if (c && (n.components.mesh?.type !== "model" || c.override)) {
|
||||
for (const mesh of root.getChildMeshes().filter(mesh => mesh.metadata?.entityId === n.id)) {
|
||||
for (const mesh of root
|
||||
.getChildMeshes()
|
||||
.filter((mesh) => mesh.metadata?.entityId === n.id)) {
|
||||
const mat = mesh.material;
|
||||
if (mat instanceof B.PBRMaterial) {
|
||||
mat.albedoColor = B.Color3.FromHexString(c.color || "#91a697");
|
||||
@@ -485,6 +579,7 @@ export class FormaRuntime {
|
||||
if (n.parent === node) n.parent = null;
|
||||
node.dispose();
|
||||
}
|
||||
this.graphics2d?.remove(id);
|
||||
this.nodes.delete(id);
|
||||
this.containers.get(id)?.dispose();
|
||||
this.containers.delete(id);
|
||||
@@ -503,9 +598,12 @@ export class FormaRuntime {
|
||||
);
|
||||
this.scene.shadowsEnabled = p.settings.shadows;
|
||||
const sky = this.scene.getLightByName("Sky");
|
||||
if (sky) sky.intensity = p.settings.rendering?.defaultLights === false ? 0 : p.settings.ambient;
|
||||
if (sky)
|
||||
sky.intensity =
|
||||
p.settings.rendering?.defaultLights === false ? 0 : p.settings.ambient;
|
||||
const sun = this.scene.getLightByName("Sun");
|
||||
if (sun) sun.intensity = p.settings.rendering?.defaultLights === false ? 0 : 2.5;
|
||||
if (sun)
|
||||
sun.intensity = p.settings.rendering?.defaultLights === false ? 0 : 2.5;
|
||||
this.engine.setHardwareScalingLevel(1 / p.settings.renderScale);
|
||||
const live = new Set(this.state.map((n) => n.id));
|
||||
for (const id of this.nodes.keys()) if (!live.has(id)) this.removeNode(id);
|
||||
@@ -515,6 +613,14 @@ export class FormaRuntime {
|
||||
n.components.mesh,
|
||||
n.components.material,
|
||||
n.components.light,
|
||||
n.components.sprite,
|
||||
n.components.tilemap,
|
||||
n.components.spriteAnimator,
|
||||
p.assets.find(
|
||||
(a) =>
|
||||
a.id ===
|
||||
(n.components.sprite?.assetId || n.components.tilemap?.assetId),
|
||||
),
|
||||
p.assets.find((a) => a.id === n.components.mesh?.assetId),
|
||||
]);
|
||||
if (signature !== this.signatures.get(n.id)) {
|
||||
@@ -530,6 +636,7 @@ export class FormaRuntime {
|
||||
node.parent = n.parentId ? this.nodes.get(n.parentId) || null : null;
|
||||
node.computeWorldMatrix(true);
|
||||
}
|
||||
this.view2d.set(this.view2d.enabled);
|
||||
this.select(this.selection);
|
||||
}
|
||||
private async createEntity(n: Entity, p: Project) {
|
||||
@@ -548,7 +655,8 @@ export class FormaRuntime {
|
||||
const a = p.assets.find((a) => a.id === m.assetId);
|
||||
if (!a?.uri) throw Error("У модели нет файла");
|
||||
let bytes: Uint8Array;
|
||||
if (this.options.readAsset) bytes = await this.options.readAsset(a.uri);
|
||||
if (this.options.readAsset)
|
||||
bytes = await this.options.readAsset(a.uri);
|
||||
else {
|
||||
const response = await fetch(a.uri);
|
||||
if (!response.ok) throw Error("Ошибка загрузки " + a.name);
|
||||
@@ -560,27 +668,45 @@ export class FormaRuntime {
|
||||
// and material animation targets independent. Mesh-only cloning can
|
||||
// retain animation targets pointing into the cached source container.
|
||||
const container = await B.LoadAssetContainerAsync(bytes, scene, {
|
||||
pluginExtension: a.name.toLowerCase().endsWith(".gltf") ? ".gltf" : ".glb",
|
||||
pluginExtension: a.name.toLowerCase().endsWith(".gltf")
|
||||
? ".gltf"
|
||||
: ".glb",
|
||||
name: a.name,
|
||||
pluginOptions: { gltf: { animationStartMode: 0 } },
|
||||
});
|
||||
if (scene.isDisposed || this.disposed) { container.dispose(); return; }
|
||||
if (scene.isDisposed || this.disposed) {
|
||||
container.dispose();
|
||||
return;
|
||||
}
|
||||
this.containers.set(n.id, container);
|
||||
// getNodes() also includes Bones, whose parent must remain a Bone.
|
||||
const roots = [...container.meshes, ...container.transformNodes, ...container.cameras, ...container.lights].filter(node => !node.parent);
|
||||
const roots = [
|
||||
...container.meshes,
|
||||
...container.transformNodes,
|
||||
...container.cameras,
|
||||
...container.lights,
|
||||
].filter((node) => !node.parent);
|
||||
container.addAllToScene();
|
||||
for (const imported of roots) imported.parent = root;
|
||||
for (const camera of container.cameras) camera.detachControl();
|
||||
this.animations.set(n.id, container.animationGroups);
|
||||
container.animationGroups.forEach(group => group.stop());
|
||||
container.animationGroups.forEach((group) => group.stop());
|
||||
this.importInfo.set(a.id, {
|
||||
...metadata,
|
||||
clips: container.animationGroups.map(group => group.name),
|
||||
cameraNames: container.cameras.map(camera => camera.name),
|
||||
triangles: container.meshes.reduce((sum, mesh) => sum + mesh.getTotalIndices() / 3, 0),
|
||||
clips: container.animationGroups.map((group) => group.name),
|
||||
cameraNames: container.cameras.map((camera) => camera.name),
|
||||
triangles: container.meshes.reduce(
|
||||
(sum, mesh) => sum + mesh.getTotalIndices() / 3,
|
||||
0,
|
||||
),
|
||||
});
|
||||
for (const warning of metadata.warnings) this.log("warning", warning, n.id);
|
||||
this.log("info", `Импорт ${a.name}: ${metadata.nodes} узлов, ${metadata.clips.length} анимаций, ${metadata.animatedProperties.length} анимированных свойств`, n.id);
|
||||
for (const warning of metadata.warnings)
|
||||
this.log("warning", warning, n.id);
|
||||
this.log(
|
||||
"info",
|
||||
`Импорт ${a.name}: ${metadata.nodes} узлов, ${metadata.clips.length} анимаций, ${metadata.animatedProperties.length} анимированных свойств`,
|
||||
n.id,
|
||||
);
|
||||
meshes = root.getChildMeshes();
|
||||
} else if (m.type === "custom" || m.type === "geometry") {
|
||||
const g =
|
||||
@@ -705,6 +831,7 @@ export class FormaRuntime {
|
||||
sign.position.y = n.components.checkpoint ? 5.2 : 0;
|
||||
sign.isPickable = false;
|
||||
}
|
||||
meshes.push(...this.graphics2d.create(n, p, root));
|
||||
for (const mesh of meshes) {
|
||||
mesh.metadata = { ...mesh.metadata, entityId: n.id };
|
||||
mesh.isPickable = true;
|
||||
@@ -723,6 +850,15 @@ export class FormaRuntime {
|
||||
this.world = new this.rapier.World({ x: 0, y: -9.81, z: 0 });
|
||||
this.world.timestep = 1 / 60;
|
||||
for (const n of this.state) this.addBody(n);
|
||||
if (
|
||||
this.state.some(
|
||||
(n) => n.components.collider2d || n.components.tilemap?.collisions,
|
||||
)
|
||||
) {
|
||||
this.physics2d = await Physics2D.create(this.document);
|
||||
for (const n of this.state) this.physics2d.add(n, this.nodes.get(n.id)!);
|
||||
this.physics2d.connect();
|
||||
}
|
||||
}
|
||||
private addBody(n: Entity) {
|
||||
const c = n.components.collider,
|
||||
@@ -804,6 +940,7 @@ export class FormaRuntime {
|
||||
this.world?.free();
|
||||
return;
|
||||
}
|
||||
this.graphics2d.start();
|
||||
this.playing = true;
|
||||
this.paused = false;
|
||||
this.runId = uid("run");
|
||||
@@ -825,7 +962,8 @@ export class FormaRuntime {
|
||||
this.currentAnims.clear();
|
||||
for (const node of this.state) {
|
||||
const animator = node.components.animator;
|
||||
if (node.enabled && animator?.autoplay) this.animate(node.id, animator.autoplay, animator.loop !== false);
|
||||
if (node.enabled && animator?.autoplay)
|
||||
this.animate(node.id, animator.autoplay, animator.loop !== false);
|
||||
}
|
||||
if (!this.options.headless && p.settings.presentation) {
|
||||
this.presentation?.dispose();
|
||||
@@ -867,6 +1005,8 @@ export class FormaRuntime {
|
||||
this.playing = false;
|
||||
this.paused = false;
|
||||
this.world?.free();
|
||||
this.physics2d?.dispose();
|
||||
this.physics2d = undefined;
|
||||
this.world = null;
|
||||
this.bodies.clear();
|
||||
this.colliders.clear();
|
||||
@@ -928,6 +1068,7 @@ export class FormaRuntime {
|
||||
if (this.workerUrl) URL.revokeObjectURL(this.workerUrl);
|
||||
}
|
||||
animate(id: string, name: string, loop = true) {
|
||||
if (this.graphics2d?.play(id, name, loop)) return;
|
||||
const groups = this.animations.get(id) || [],
|
||||
n = this.state.find((n) => n.id === id),
|
||||
mapped = n?.components.animator?.[name.toLowerCase()] || name,
|
||||
@@ -942,6 +1083,7 @@ export class FormaRuntime {
|
||||
this.currentAnims.set(id, target.name);
|
||||
}
|
||||
previewAnimation(id: string, name: string) {
|
||||
if (this.graphics2d?.play(id, name, undefined, true)) return;
|
||||
this.animations.get(id)?.forEach((g) => g.stop());
|
||||
this.currentAnims.delete(id);
|
||||
this.animate(id, name, !["Attack", "Death"].includes(name));
|
||||
@@ -957,8 +1099,17 @@ export class FormaRuntime {
|
||||
!next.transform.rotation.every(Number.isFinite)
|
||||
)
|
||||
throw Error("Скрипт вернул некорректную трансформацию");
|
||||
validate2D(next, this.document, this.state);
|
||||
Object.assign(n, next);
|
||||
this.applyTransform(n);
|
||||
this.physics2d?.patch(
|
||||
n.id,
|
||||
!!c.patch.transform?.position,
|
||||
!!c.patch.transform?.rotation,
|
||||
);
|
||||
if (c.patch.enabled !== undefined)
|
||||
for (const id of this.physics2d?.entries.keys() || [])
|
||||
this.physics2d!.setEnabled(id);
|
||||
const root = this.nodes.get(n.id)!,
|
||||
body = this.bodies.get(n.id);
|
||||
if (body && c.patch.transform?.position) {
|
||||
@@ -981,8 +1132,17 @@ export class FormaRuntime {
|
||||
);
|
||||
}
|
||||
} else if (c.type === "velocity" && n) {
|
||||
this.motors.get(n.id)?.set(c.value);
|
||||
if (this.physics2d?.entries.has(n.id))
|
||||
this.physics2d.velocity(n.id, c.value);
|
||||
else this.motors.get(n.id)?.set(c.value);
|
||||
} else if (c.type === "impulse2d" && n) {
|
||||
this.physics2d?.impulse(n.id, c.value);
|
||||
} else if (c.type === "teleport" && n) {
|
||||
if (this.physics2d?.entries.has(n.id)) {
|
||||
this.physics2d.teleport(n.id, c.position);
|
||||
this.moves.delete(n.id);
|
||||
continue;
|
||||
}
|
||||
const motor = this.motors.get(n.id);
|
||||
if (!motor) throw Error("Teleport requires a character component");
|
||||
motor.teleport(c.position);
|
||||
@@ -1056,13 +1216,24 @@ export class FormaRuntime {
|
||||
if (!n.parentId && position) n.transform.position = position;
|
||||
await this.createEntity(n, this.document);
|
||||
}
|
||||
if (
|
||||
!this.physics2d &&
|
||||
nodes.some(
|
||||
(n) => n.components.collider2d || n.components.tilemap?.collisions,
|
||||
)
|
||||
)
|
||||
this.physics2d = await Physics2D.create(this.document);
|
||||
for (const n of nodes) {
|
||||
this.nodes.get(n.id)!.parent = n.parentId
|
||||
? this.nodes.get(n.parentId)!
|
||||
: null;
|
||||
this.state.push(n);
|
||||
this.addBody(n);
|
||||
this.physics2d?.add(n, this.nodes.get(n.id)!);
|
||||
const autoplay = n.components.spriteAnimator?.autoplay;
|
||||
if (autoplay) this.graphics2d.play(n.id, autoplay);
|
||||
}
|
||||
this.physics2d?.connect();
|
||||
}
|
||||
private swing(n: Entity) {
|
||||
const p = this.nodes.get(n.id)?.getAbsolutePosition();
|
||||
@@ -1129,6 +1300,7 @@ export class FormaRuntime {
|
||||
);
|
||||
}
|
||||
}
|
||||
this.physics2d?.step(1 / 60, this.moves, count);
|
||||
this.world.step();
|
||||
this.accumulator -= 1 / 60;
|
||||
}
|
||||
@@ -1154,7 +1326,7 @@ export class FormaRuntime {
|
||||
}
|
||||
}
|
||||
for (const [id, d] of this.moves)
|
||||
if (!this.bodies.has(id)) {
|
||||
if (!this.bodies.has(id) && !this.physics2d?.entries.has(id)) {
|
||||
const n = this.state.find((n) => n.id === id);
|
||||
if (n) {
|
||||
n.transform.position = n.transform.position.map(
|
||||
@@ -1197,6 +1369,7 @@ export class FormaRuntime {
|
||||
})),
|
||||
]),
|
||||
),
|
||||
sprites: this.graphics2d?.snapshot(),
|
||||
imports: Object.fromEntries(this.importInfo),
|
||||
logs: this.logs.slice(-30),
|
||||
fps: Math.round(this.engine.getFps()),
|
||||
@@ -1247,6 +1420,8 @@ export class FormaRuntime {
|
||||
if (this.automation && now < this.automation.until)
|
||||
Object.assign(input, this.automation);
|
||||
else this.automation = null;
|
||||
(input as any).y = this.automation?.y ?? input.z;
|
||||
this.physics2d?.setInput(input);
|
||||
this.physics(dt);
|
||||
this.scriptTime += dt;
|
||||
if (this.ready && !this.pending) {
|
||||
@@ -1256,6 +1431,7 @@ export class FormaRuntime {
|
||||
entities: this.state,
|
||||
input,
|
||||
physics: this.physicsSnapshot(),
|
||||
events2d: this.physics2d?.drainEvents() || [],
|
||||
dt: Math.min(0.1, this.scriptTime),
|
||||
});
|
||||
this.scriptTime = 0;
|
||||
@@ -1267,7 +1443,12 @@ export class FormaRuntime {
|
||||
target = c?.targetId
|
||||
? this.nodes.get(c.targetId)?.getAbsolutePosition()
|
||||
: B.Vector3.Zero();
|
||||
if (c?.mode === "fixed") {
|
||||
if (
|
||||
c?.mode === "2d" ||
|
||||
(!c && activeScene(this.document).mode === "2d")
|
||||
) {
|
||||
this.updateCamera2D();
|
||||
} else if (c?.mode === "fixed") {
|
||||
this.updateFixedCamera();
|
||||
} else if (target && c?.mode === "firstPerson") {
|
||||
const player = this.state.find((n) => n.id === c.targetId);
|
||||
@@ -1323,6 +1504,13 @@ export class FormaRuntime {
|
||||
if (t <= 0) this.flashes.delete(id);
|
||||
else this.flashes.set(id, t - dt);
|
||||
}
|
||||
this.view2d.update();
|
||||
this.graphics2d.tick(
|
||||
dt,
|
||||
this.playing,
|
||||
this.paused,
|
||||
this.physics2d?.snapshot(),
|
||||
);
|
||||
try {
|
||||
this.scene.animationsEnabled = !(this.playing && this.paused);
|
||||
this.scene.render();
|
||||
@@ -1339,6 +1527,12 @@ export class FormaRuntime {
|
||||
0,
|
||||
),
|
||||
firstPerson: this.firstPerson(),
|
||||
twoD: this.state.some(
|
||||
(n) =>
|
||||
n.enabled &&
|
||||
n.components.character2d?.controls !== false &&
|
||||
n.components.character2d,
|
||||
),
|
||||
playing: this.playing,
|
||||
});
|
||||
}
|
||||
@@ -1351,6 +1545,9 @@ export class FormaRuntime {
|
||||
this.cleanup.forEach((fn) => fn());
|
||||
this.world?.free();
|
||||
this.world = null;
|
||||
this.physics2d?.dispose();
|
||||
this.graphics2d?.dispose();
|
||||
this.view2d.dispose();
|
||||
this.scene?.dispose();
|
||||
this.engine.dispose();
|
||||
}
|
||||
@@ -1365,13 +1562,67 @@ export class FormaRuntime {
|
||||
this.gameCamera.setTarget(B.Vector3.FromArray(c.lookAt || [0, 0, 0]));
|
||||
this.gameCamera.fov = c.fov || 0.72;
|
||||
}
|
||||
private updateCamera2D() {
|
||||
const node = this.state.find((n) => n.enabled && n.components.camera),
|
||||
c = node?.components.camera || {};
|
||||
const follow = c.targetId
|
||||
? this.nodes.get(c.targetId)?.getAbsolutePosition()
|
||||
: null;
|
||||
const pos =
|
||||
follow ||
|
||||
(node ? this.nodes.get(node.id)?.getAbsolutePosition() : null) ||
|
||||
B.Vector3.Zero();
|
||||
let x = pos.x + (follow ? c.offset?.[0] || 0 : 0),
|
||||
y = pos.y + (follow ? c.offset?.[1] || 0 : 0);
|
||||
if (c.bounds) {
|
||||
x = Math.max(c.bounds[0], Math.min(c.bounds[2], x));
|
||||
y = Math.max(c.bounds[1], Math.min(c.bounds[3], y));
|
||||
}
|
||||
this.gameCamera.position.copyFromFloats(
|
||||
x,
|
||||
y,
|
||||
follow ? pos.z + (c.distance || 20) : Math.max(20, pos.z),
|
||||
);
|
||||
|
||||
this.gameCamera.mode = B.Camera.ORTHOGRAPHIC_CAMERA;
|
||||
const width = this.engine.getRenderWidth(),
|
||||
height = Math.max(1, this.engine.getRenderHeight()),
|
||||
ppu = c.pixelsPerUnit || 100;
|
||||
const w = c.pixelPerfect
|
||||
? width /
|
||||
(ppu * Math.max(1, Math.floor(width / ((c.orthoWidth || 20) * ppu))))
|
||||
: c.orthoWidth || 20;
|
||||
this.gameCamera.orthoLeft = -w / 2;
|
||||
this.gameCamera.orthoRight = w / 2;
|
||||
this.gameCamera.orthoTop = (w * height) / width / 2;
|
||||
this.gameCamera.orthoBottom = (-w * height) / width / 2;
|
||||
this.gameCamera.viewport = new B.Viewport(0, 0, 1, 1);
|
||||
if (c.pixelPerfect) {
|
||||
this.gameCamera.position.x = Math.round(x * ppu) / ppu;
|
||||
this.gameCamera.position.y = Math.round(y * ppu) / ppu;
|
||||
}
|
||||
this.gameCamera.setTarget(
|
||||
this.gameCamera.position.add(new B.Vector3(0, 0, -20)),
|
||||
);
|
||||
}
|
||||
private updateProjection() {
|
||||
const cameraEntity = this.state.find(n => n.enabled && n.components.camera);
|
||||
const cameraEntity = this.state.find(
|
||||
(n) => n.enabled && n.components.camera,
|
||||
);
|
||||
const c = cameraEntity?.components.camera;
|
||||
if (c?.mode === "2d" || (!c && activeScene(this.document).mode === "2d")) {
|
||||
this.scene.activeCamera = this.gameCamera;
|
||||
this.updateCamera2D();
|
||||
return;
|
||||
}
|
||||
if (cameraEntity && c?.mode === "imported") {
|
||||
const cameras = this.containers.get(cameraEntity.id)?.cameras || [];
|
||||
const imported = cameras.find(camera => camera.name === c.cameraName) || cameras[0];
|
||||
if (imported) { this.scene.activeCamera = imported; return; }
|
||||
const imported =
|
||||
cameras.find((camera) => camera.name === c.cameraName) || cameras[0];
|
||||
if (imported) {
|
||||
this.scene.activeCamera = imported;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.scene.activeCamera = this.gameCamera;
|
||||
const cam = this.gameCamera;
|
||||
|
||||
Reference in New Issue
Block a user