Rezepte
Vollständige Skripte für Aufgaben, die in fast jedem Projekt vorkommen. Kopieren Sie eines, hängen Sie es an ein Objekt und passen Sie die Zahlen an.
Neu bei Skripten? Fangen Sie mit Erstes Skript an. Dort werden die Teile erklärt, die alle diese Skripte gemeinsam haben.
Jedes Skript importiert die Komponentenklassen, die es nutzt, aus @was/engine. init und
defineProps brauchen keinen Import.
Ein Objekt drehen
import { TransformComponent } from '@was/engine';
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 → Radiant
t.update({
rotation: { ...t.$data.rotation, y: t.$data.rotation.y + turn * dt },
});
});
});
dt multiplizierendt ist die Dauer des letzten Frames. Wenn Sie damit multiplizieren, dreht sich das Objekt auf
schnellen und langsamen Handys gleich schnell pro Sekunde. Ohne diese Multiplikation hängt das Tempo
vom Gerät ab.
Auf und ab schweben
import { TransformComponent } from '@was/engine';
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 bewegt sich gleichmäßig zwischen −1 und 1 hin und her, immer wieder. Multiplizieren Sie
das Ergebnis, um zu ändern, wie weit sich das Objekt bewegt. Multiplizieren Sie die Zeit, um zu
ändern, wie schnell.
Antippen zum Einsammeln
init((ctx) => {
const props = defineProps({
points: { type: 'number', default: 1 },
sound: { type: 'resource', resource: 'audio' },
});
ctx.on('on-click', () => {
if (props.sound) ctx.audio.play(props.sound.id);
ctx.setGlobal('score', (ctx.getGlobal<number>('score') ?? 0) + props.points);
ctx.destroy(ctx.entity);
});
});
Der Punktestand liegt in einem globalen Wert. So können auch andere Teile ihn lesen, etwa ein Zähler auf dem Bildschirm oder eine Prüfung, ob gewonnen ist.
Den Punktestand anzeigen
Legen Sie dieses Skript auf das Objekt, das Ihre UI-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); // Startwert anzeigen
});
Ihr Skript verwaltet die Zahlen, und die Karte zeigt sie nur an. Liegt der Punktestand in der Karte, funktioniert er nicht mehr, sobald etwas anderes ihn braucht.
Countdown
Zählt auf einer UI-Karte rückwärts. Ist die Zeit um, sendet das Skript das Signal time-up. Dann
läuft jedes Ereignis, dessen Trigger Signal auf time-up eingestellt ist.
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.step('emit_signal', { signal: 'time-up', scope: 'broadcast' });
}
ui.set('time', `${Math.ceil(left)}`);
});
});
Alle paar Sekunden wiederholen
init((ctx) => {
const props = defineProps({
thing: { type: 'resource', resource: 'scene', label: 'Model' },
every: { type: 'number', default: 3, suffix: 's' },
});
let since = 0;
ctx.tick((dt) => {
since += dt;
if (since < props.every) return;
since = 0;
if (props.thing) ctx.spawn(props.thing, { parent: ctx.scene });
});
});
Skripte haben kein setInterval. Zählen Sie stattdessen die Sekunden in tick. Das hört von
selbst auf, wenn das Objekt entfernt wird.
Ein 3D-Modell ist eine Ressource vom Typ scene. Eine Einstellung für ein Modell schreiben Sie
deshalb als { type: 'resource', resource: 'scene' }.
Ein Objekt mit Skript erzeugen
ctx.spawn baut ein Objekt aus einer Ressource. Erzeugen Sie so ein Modell, bekommen Sie nur
das Modell, ohne Skript.
Für einen Gegner, der verfolgt, eine Kugel, die fliegt, oder ein Sammelobjekt, das reagiert, bauen Sie das Objekt selbst und fügen das Skript hinzu:
import {
ColliderComponent,
ModelRefComponent,
RigidBodyComponent,
ScriptComponent,
TransformComponent,
} from '@was/engine';
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 erzeugte Objekt bekommt seine eigene Kopie des Skripts, mit eigenen Variablen. So hat jeder Gegner eigene Lebenspunkte, einen eigenen Zustand und eigene Timer, ohne zusätzlichen Aufwand:
// 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 hat nur die Komponenten, die Sie aufzählen. Ohne Collider
kollidiert es nicht. Ohne Starrkörper bewegt die Physik es nicht.
Das nächste Objekt finden
Nützlich für einen Turm, einen Gegner oder alles, was sich ein Ziel sucht:
import { Entity, TransformComponent } from '@was/engine';
const nearest = (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;
};
Die Kandidaten bekommen Sie mit ctx.query(...). Fragen Sie nach einer Komponente, die nur Gegner
haben. Oder vergeben Sie beim Erzeugen
Tags und filtern Sie nach Tag.
Dem Spieler folgen
Bewegt sich auf ein Ziel zu und hält in einem festgelegten Abstand an:
import { TransformComponent } from '@was/engine';
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 },
});
});
});
Wenn Sie durch gap teilen, ist die Richtung genau 1 Einheit lang. Multiplizieren Sie sie dann mit
step, bewegt sich das Objekt genau so weit. Das brauchen Sie oft.
Schießen, wohin Sie schauen
import { MetaComponent } from '@was/engine';
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);
});
});
Sie brauchen await, weil die Antwort erst im nächsten Frame kommt.
Ein Physikobjekt werfen
import {
ColliderComponent,
InitialVelocityComponent,
ModelRefComponent,
RigidBodyComponent,
TransformComponent,
} from '@was/engine';
init((ctx) => {
const props = defineProps({
ball: { type: 'resource', resource: 'scene', label: 'Ball' },
speed: { type: 'number', default: 6, suffix: 'm/s' },
});
ctx.on('on-click', () => {
const pose = ctx.camera.pose();
if (!pose || !props.ball) return;
const { x, y, z } = pose.forward;
const ball = ctx.create({
name: 'Ball',
parent: ctx.scene,
components: [
new ModelRefComponent({ referal: props.ball.id }),
new RigidBodyComponent({ type: 'dynamic' }),
new ColliderComponent({ type: 'sphere', radius: 0.1 }),
// die Geschwindigkeit, mit der der Körper startet, einmal angewendet
new InitialVelocityComponent({
velocity: { x: x * props.speed, y: y * props.speed, z: z * props.speed },
}),
],
});
ball.getComponent(TransformComponent)?.update({ position: pose.position });
});
});
Die Pose der Kamera gilt in Weltkoordinaten, die Position des Balls wird von seiner Szene aus gemessen. In einer 3D-Szene ist beides dasselbe.
Die Physik liest Position und Anfangsgeschwindigkeit einmal, wenn sie den Körper anlegt. Danach
bewirkt es nichts mehr, die Position zu setzen. Platzieren Sie den Körper mit ctx.physics.teleport
und stoßen Sie ihn mit ctx.physics.applyImpulse an.
Eine Tür einmal öffnen
import { TransformComponent } from '@was/engine';
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 macht die Änderung fließend statt sofort. Alles, was Sie darin ändern, bewegt
sich gemeinsam.
Auf eine Kollision reagieren
import { MetaComponent } from '@was/engine';
init((ctx) => {
const props = defineProps({
thud: { type: 'resource', resource: 'audio' },
});
ctx.on('on-collide', ({ other }) => {
const hit = ctx.get(other);
const name = hit?.getComponent(MetaComponent)?.$data.name ?? '';
if (name !== 'Player') return;
if (props.thud) ctx.audio.play(props.thud.id);
ctx.postMessage('player-hit', { by: ctx.entity.id });
});
});
postMessage teilt anderen Skripten mit, was passiert ist. Keines der Skripte braucht einen
Verweis auf das andere.
Auf das Tracking warten
Spielt eine Animation ab, wenn die Szene gefunden wird, und stoppt sie, wenn das Tracking verloren geht:
init((ctx) => {
const props = defineProps({
animation: { type: 'string', default: 'Intro', label: 'Animation name' },
});
// Die Schritte erwarten die ID der Animation. Sie wird über den Namen im Panel Animation gefunden.
const presets = ctx.entity.getComponent('TemporalStateComponent')?.$data.presets ?? [];
const presetId = presets.find(
(p: { id: string; name: string }) => p.name === props.animation,
)?.id;
if (!presetId) return;
ctx.on('on-detect', () => ctx.step('play_animation', { presetId }));
ctx.on('on-lost', () => ctx.step('stop_animation', { presetId }));
});
Alles, was der Editor als Schritt kann, führt ein Skript mit einem Aufruf aus: Animationen, Übergänge, Zustände, Navigation. Sehen Sie die Schritte durch, bevor Sie etwas von Hand bauen.
Produktkonfigurator
Ein Konfigurator tut zwei Dinge: Er ändert, was zu sehen ist, und merkt sich, was der Besucher gewählt hat.
import { MaterialComponent } from '@was/engine';
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 Material-Slot kann auf ein gespeichertes Material verweisen, statt eigene 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
});
Um statt einer Ausführung ein ganzes Modell zu tauschen, fügen Sie defineProps eine Liste von
Modellen hinzu und ändern in choose den Wert referal an der Modell-Komponente:
import { ModelRefComponent } from '@was/engine';
// in defineProps
models: { type: 'array', item: { type: 'resource', resource: 'scene' }, label: 'Models' },
// in choose(index)
ctx.entity.getComponent(ModelRefComponent)?.update({ referal: props.models[index]?.id ?? null });
Ein Konfigurator ist meist eine einfache 3D-Szene ohne AR. Sie öffnet sich sofort, läuft auf
jedem Gerät und braucht keinen Marker. Stellen Sie die Kamera auf orbit und begrenzen Sie, wie
weit sie kippen kann (den Polarwinkel), damit niemand das Produkt von unten sieht.
→ Kameras
Lassen Sie Besucher ein Foto ihrer Auswahl machen, damit sie es teilen können. → Projekteinstellungen
Wo Sie Werte aufbewahren
| Der Wert… | Legen Sie ihn in |
|---|---|
| wird nur von diesem Skript genutzt | eine einfache Variable in init |
| wird nach einem Szenenwechsel noch gebraucht | ctx.store |
| wird von anderen Skripten, Ereignissen oder Patches gebraucht | ctx.setGlobal |
| ist ein Ereignis, auf das andere reagieren sollen | ctx.postMessage |
| wird mit anderen Personen in einer Sitzung geteilt | ctx.net.state |
Wenn Sie einen Wert beim nächsten Mal brauchen, schicken Sie ihn an Ihren eigenen Server, solange der Besucher noch auf der Seite ist.
Weiter: Mehrspieler — Skripte für mehrere Personen in einer Sitzung.