Zum Hauptinhalt springen

Was Sie wirklich bauen werden

Jedes Skript hier ist vollständig. Kopieren, an ein Objekt hängen, die Zahlen ändern.

Wenn Sie noch keines geschrieben haben, beginnen Sie mit Ihrem ersten Skript — dort wird die Form erklärt, die alle hier teilen.

Etwas endlos drehen

init((ctx) => {
const props = defineProps({
speed: { type: 'number', default: 45, suffix: '°/s' },
});

ctx.tick((dt) => {
const t = ctx.entity.getComponent(TransformComponent);
if (!t) return;
const turn = (props.speed * Math.PI) / 180; // Grad → Bogenmaß
t.update({
rotation: { ...t.$data.rotation, y: t.$data.rotation.y + turn * dt },
});
});
});
Warum mit dt multiplizieren

dt ist, wie lange das letzte Bild gedauert hat. Damit zu multiplizieren heißt, dass sich das Objekt pro Sekunde gleich weit dreht — auf einem schnellen Telefon wie auf einem langsamen. Lassen Sie es weg, läuft Ihre Animation in dem Tempo, das das Gerät zufällig schafft.

Etwas sanft wippen lassen

init((ctx) => {
const t = ctx.entity.getComponent(TransformComponent);
const startY = t?.$data.position.y ?? 0;

ctx.tick((dt, time) => {
t?.update({
position: { ...t.$data.position, y: startY + Math.sin(time * 2) * 0.1 },
});
});
});

Math.sin schwingt endlos weich zwischen −1 und 1. Multiplizieren Sie es, um die Weite zu setzen, multiplizieren Sie die Zeit, um das Tempo zu setzen.

Antippen und einsammeln

init((ctx) => {
const props = defineProps({
points: { type: 'number', default: 1 },
sound: { type: 'resource', resource: 'audio' },
});

ctx.on('on-click', () => {
ctx.audio.play(props.sound);
ctx.setGlobal('score', ((ctx.getGlobal<number>('score') ?? 0) + props.points));
ctx.destroy(ctx.entity);
});
});

Der Punktestand lebt in einem Global, damit alles andere (eine Anzeige auf dem Bildschirm, die Siegbedingung) ihn lesen kann.

Diesen Punktestand anzeigen

Legen Sie das auf das Objekt, das Ihre Interface-Karte trägt:

init((ctx) => {
const ui = ctx.getDivKit(ctx.entity);

ctx.subscribeGlobal('score', (value) => {
ui.set('score', Number(value ?? 0));
});

ui.set('score', ctx.getGlobal<number>('score') ?? 0); // den Startwert zeigen
});
Das ist das Muster, das man sich merkt

Ihre Logik besitzt die Zahlen; die Karte zeigt sie nur an. Den Punktestand in der Karte halten zu wollen bricht zusammen, sobald irgendetwas anderes ihn braucht.

Ein Countdown

init((ctx) => {
const props = defineProps({ seconds: { type: 'number', default: 60 } });
const ui = ctx.getDivKit(ctx.entity);

let left = props.seconds;
let finished = false;

ctx.tick((dt) => {
if (finished) return;
left -= dt;

if (left <= 0) {
finished = true;
left = 0;
ctx.emit('on-signal', { signal: 'time-up' });
}

ui.set('time', `${Math.ceil(left)}`);
});
});

Alle paar Sekunden etwas tun

init((ctx) => {
const props = defineProps({ every: { type: 'number', default: 3, suffix: 's' } });
let since = 0;

ctx.tick((dt) => {
since += dt;
if (since < props.every) return;
since = 0;

ctx.spawn(props.thing, { parent: ctx.scene });
});
});

Hier gibt es kein setInterval. Sekunden in tick zu zählen ist das Äquivalent, und es hört von selbst auf, wenn das Objekt verschwindet.

Etwas spawnen, das eigenes Verhalten hat

Dieser erwischt die Leute. ctx.spawn baut ein Objekt aus einer Ressource — ein Modell zu spawnen gibt Ihnen also ein Modell, und sonst nichts. Ein Skript bringt es nicht mit.

Für einen Gegner, der verfolgt, eine Kugel, die fliegt, ein Sammelstück, das reagiert, bauen Sie das Objekt selbst und hängen das Verhalten an:

init((ctx) => {
const props = defineProps({
model: { type: 'resource', resource: 'scene' },
brain: { type: 'resource', resource: 'script' },
});

const spawnEnemy = (x: number, z: number) => {
const enemy = ctx.create({
name: 'Enemy',
parent: ctx.scene,
components: [
new ModelRefComponent({ referal: props.model?.id ?? null }),
new ScriptComponent({ referal: props.brain?.id ?? null }),
new RigidBodyComponent({ type: 'dynamic' }),
new ColliderComponent({ type: 'capsule', radius: 0.3, height: 1.6 }),
],
});

enemy.getComponent(TransformComponent)?.update({ position: { x, y: 0, z } });
return enemy;
};

ctx.on('on-launch', () => spawnEnemy(2, 0));
});

Jedes gespawnte Objekt bekommt seine eigene Instanz dieses Skripts mit eigenen Variablen — Lebenspunkte, Zustand und Timer pro Gegner brauchen also nichts Besonderes:

// das eigene Skript des Gegners
init((ctx) => {
let health = 3;

ctx.on('on-collide', () => {
health -= 1;
if (health <= 0) ctx.destroy(ctx.entity);
});
});
Geben Sie ihm auch einen Collider, wenn die Physik ihn sehen soll

Ein mit create gebautes Objekt ist genau die Komponenten, die Sie aufgezählt haben. Vergessen Sie den Collider, kollidiert es nicht; vergessen Sie den Starrkörper, bewegt die Physik es nicht.

Das Nächstgelegene finden

Die andere Hälfte eines Turms, eines Gegners oder von allem, was zielt:

const nearest = (ctx, from: Entity, candidates: Entity[]) => {
const a = from.getComponent(TransformComponent)?.$data.position;
if (!a) return undefined;

let best: Entity | undefined;
let bestGap = Infinity;

for (const c of candidates) {
const b = c.getComponent(TransformComponent)?.$data.position;
if (!b) continue;
const gap = Math.hypot(b.x - a.x, b.y - a.y, b.z - a.z);
if (gap < bestGap) { bestGap = gap; best = c; }
}
return best;
};

Holen Sie die Kandidaten mit ctx.query(...) — fragen Sie nach einer Komponente, die nur Gegner tragen, oder markieren Sie sie beim Erzeugen mit Tags und filtern danach.

Dem Spieler folgen, aber nicht zu dicht

init((ctx) => {
const props = defineProps({
target: { type: 'entity' },
distance: { type: 'number', default: 2 },
speed: { type: 'number', default: 1.5 },
});

ctx.tick((dt) => {
const me = ctx.entity.getComponent(TransformComponent);
const them = props.target?.getComponent(TransformComponent);
if (!me || !them) return;

const a = me.$data.position;
const b = them.$data.position;
const dx = b.x - a.x;
const dz = b.z - a.z;
const gap = Math.hypot(dx, dz);
if (gap <= props.distance) return; // nah genug

const step = Math.min(props.speed * dt, gap - props.distance);
me.update({
position: { ...a, x: a.x + (dx / gap) * step, z: a.z + (dz / gap) * step },
});
});
});

Durch gap zu teilen macht aus der Richtung eine Länge von genau 1, das Multiplizieren mit step bewegt also exakt so weit. Dieser Trick kommt ständig vor.

Schießen, worauf Sie schauen

init((ctx) => {
ctx.on('on-click', async () => {
const hits = await ctx.raycast();
const hit = hits[0];
if (!hit?.entity) return;

const meta = hit.entity.getComponent(MetaComponent);
if (meta?.$data.name.startsWith('Target')) ctx.destroy(hit.entity);
});
});

await ist nötig, weil der Strahl gegen die echte Szene verfolgt wird und die Antwort im nächsten Bild zurückkommt.

Ein Physikobjekt werfen

init((ctx) => {
const props = defineProps({ force: { type: 'number', default: 6 } });

ctx.on('on-click', () => {
const pose = ctx.camera.pose();
if (!pose) return;

const ball = ctx.spawn(props.ball, { parent: ctx.scene });
ctx.physics.teleport(ball, pose.position);
ctx.physics.applyImpulse(ball, {
x: pose.forward.x * props.force,
y: pose.forward.y * props.force,
z: pose.forward.z * props.force,
});
});
});
Platzieren mit teleport, schieben mit applyImpulse

Die Position eines Physikobjekts direkt zu setzen bewirkt nichts — die Physik besitzt seinen Ort und überschreibt Sie im nächsten Schritt.

Eine Tür, die sich einmal öffnet

init((ctx) => {
let open = false;

ctx.on('on-click', () => {
if (open) return;
open = true;

ctx.startTransition({ durationMs: 600, easing: 'ease-out' }, () => {
const t = ctx.entity.getComponent(TransformComponent);
t?.update({ rotation: { ...t.$data.rotation, y: Math.PI / 2 } });
});
});
});

startTransition lässt die Änderung gleiten statt zu springen. Alles, was Sie darin ändern, läuft gemeinsam weich.

Auf eine Kollision reagieren

init((ctx) => {
ctx.on('on-collide', ({ other }) => {
const hit = ctx.get(other);
const name = hit?.getComponent(MetaComponent)?.$data.name ?? '';
if (name !== 'Player') return;

ctx.audio.play(props.thud);
ctx.postMessage('player-hit', { by: ctx.entity.id });
});
});

postMessage sagt es anderen Skripten, ohne dass eines von beiden eine Referenz auf das andere braucht.

Warten, bis die Szene getrackt wird

init((ctx) => {
ctx.on('on-detect', () => {
ctx.step('play_animation', { presetId: 'intro' });
});

ctx.on('on-lost', () => {
ctx.step('stop_animation', { presetId: 'intro' });
});
});
ctx.step erspart Ihnen, die Engine nachzubauen

Alles, was der Editor als Schritt kann (Animationen, Transitionen, States, Navigation), kann ein Skript mit einem Aufruf auslösen. Sehen Sie in der Schritt-Referenz nach, bevor Sie etwas von Hand bauen.

Ein Produktkonfigurator

Der häufigste kommerzielle Bau auf dieser Plattform, und er besteht im Wesentlichen aus zwei Bewegungen: tauschen, was gezeigt wird, und merken, was gewählt wurde.

init((ctx) => {
const props = defineProps({
variants: {
type: 'array',
item: { type: 'resource', resource: 'material' },
label: 'Finishes',
},
prices: { type: 'array', item: { type: 'number' }, label: 'Price per finish' },
});

const ui = ctx.getDivKit(ctx.entity);
const material = ctx.entity.getComponent(MaterialComponent);

const choose = (index: number) => {
const finish = props.variants[index];
if (!finish || !material) return;

// Ein Materialslot kann auf ein gespeichertes Material zeigen, statt Werte zu tragen.
material.update({
materials: [{ ...material.$data.materials[0], type: 'ref', referal: finish.id }],
});

ctx.setGlobal('finish', index);
ui.set('price', props.prices[index] ?? 0);
};

ui.onAction('finish-0', () => choose(0));
ui.onAction('finish-1', () => choose(1));
ui.onAction('finish-2', () => choose(2));

ui.onAction('buy', () => {
const index = ctx.getGlobal<number>('finish') ?? 0;
ctx.step('url_transit_action', {
url: `https://shop.example.com/chair?finish=${index}`,
transitionType: 'new_tab',
});
});

choose(0); // mit der ersten Ausführung starten
});

Ein ganzes Modell statt einer Oberfläche zu tauschen hat dieselbe Form; man schreibt referal stattdessen auf die Modellkomponente:

ctx.entity.getComponent(ModelRefComponent)?.update({ referal: props.models[index]?.id ?? null });
Geben Sie ihm eine Kamera, die der Besucher drehen kann, und kein AR

Ein Konfigurator ist meist eine schlichte 3D-Szene: Sie öffnet sofort, funktioniert auf jedem Gerät und braucht keinen Marker. Stellen Sie die Kamera auf orbit und begrenzen Sie den Polarwinkel, damit niemand am Ende von unten hineinschaut. → Kameras

Schalten Sie die Fotoaufnahme ein

Ein Konfigurator, den man abfotografieren kann, ist ein Konfigurator, den man teilt. → Projekteinstellungen

Wohin mit Ihrem Zustand

Der Wert ist…Legen Sie ihn in
nur von diesem Skript benutzteine schlichte Variable in init
nach einem Szenenwechsel noch nötigctx.store
von anderen Skripten, Ereignissen oder Patches gebrauchtctx.setGlobal
ein Ereignis, auf das andere reagieren sollenctx.postMessage
mit anderen Leuten in einem Raum geteiltctx.net.state
Nichts davon überlebt das Schließen des Tabs

Muss etwas beim nächsten Mal da sein, schicken Sie es an Ihren eigenen Server, solange der Besucher noch auf der Seite ist.


Weiter: Mehrspieler