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 },
});
});
});
dt multiplizierendt 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
});
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);
});
});
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,
});
});
});
teleport, schieben mit applyImpulseDie 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 nachzubauenAlles, 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 });
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
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 benutzt | eine schlichte Variable in init |
| nach einem Szenenwechsel noch nötig | ctx.store |
| von anderen Skripten, Ereignissen oder Patches gebraucht | ctx.setGlobal |
| ein Ereignis, auf das andere reagieren sollen | ctx.postMessage |
| mit anderen Leuten in einem Raum geteilt | ctx.net.state |
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