Zum Hauptinhalt springen

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 },
});
});
});
Warum mit dt multiplizieren

dt 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
});
Behalten Sie die Zahlen im Skript

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);
});
});
Einen Collider hinzufügen, wenn die Physik das Objekt sehen soll

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.

Den Start festlegen, bevor die Physik übernimmt

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 }));
});
Erst die Schritte ansehen

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 });
Eine 3D-Szene und eine Orbit-Kamera nutzen

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

Fotoaufnahme einschalten

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 genutzteine einfache Variable in init
wird nach einem Szenenwechsel noch gebrauchtctx.store
wird von anderen Skripten, Ereignissen oder Patches gebrauchtctx.setGlobal
ist ein Ereignis, auf das andere reagieren sollenctx.postMessage
wird mit anderen Personen in einer Sitzung geteiltctx.net.state
Wer den Tab schließt, verliert alles

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.