Zum Hauptinhalt springen

Die ctx-API

Über ctx arbeitet Ihr Skript mit der laufenden Szene. Auf dieser Seite steht alles, was damit möglich ist.

init übergibt ctx an Ihr Skript. Die API ist absichtlich klein. Wenn Sie hier etwas nicht finden, suchen Sie nach einem eingebauten Schritt, der es erledigt.

Wo Sie sind​

NameWas es ist
ctx.entitydas Objekt, an dem dieses Skript hängt
ctx.scenedie Szene, in der das Objekt liegt
ctx.spaceder Raum — die ganze Welt, in der sich der Besucher befindet

Frames und Trigger​

ctx.tick((dt, t) => {});          // jeden Frame; dt und t in Sekunden
ctx.effect(() => {}); // läuft erneut, wenn sich Gelesenes ändert; darf eine Aufräumfunktion zurückgeben
ctx.on(trigger, (payload) => {}); // auf einen Trigger reagieren
ctx.emit(trigger, payload); // einen Trigger von diesem Objekt senden

Die meisten Trigger heißen hier genauso wie in Ereignissen. Drei Eingabe-Trigger mit Zweigen kommen in einem Skript als getrennte Trigger an:

  • Taste (on-key) kommt als on-keydown und on-keyup an;
  • Druck auf das Objekt (on-mouse) kommt als on-mousedown, on-mouseup und on-click an;
  • Hover (on-hover) kommt als on-hover und on-blur an.

Die Trigger für Zeit, Entfernung, Sichtbarkeit, Signale und Variablen funktionieren nur in Ereignissen. Nutzen Sie in einem Skript stattdessen ctx.tick, ctx.subscribeGlobal und ctx.handleMessage.

Diese Trigger liefern nützliche Daten:

TriggerWas Sie bekommen
on-keydown · on-keyup{ code, ctrl, shift, alt, meta }
on-state-active · on-state-inactive{ stateId }
on-collide{ other } — womit Sie zusammengestoßen sind
on-divkit-action{ id, … } — welche Schaltfläche
on-game-control{ state } — Stehen, Gehen, Rennen oder Springen
on-drag · on-pinch · on-rotate{ dx, dy } · { scale } · { angle }
on-vps-localizedwo sich der Besucher tatsächlich befindet

on-launch kommt an, sobald Ihr Skript startet. Sie können es also nicht verpassen.

Objekte finden​

ctx.get(id);
ctx.findByName('Door');
ctx.find(LightComponent); // das erste Objekt mit diesen Komponenten
ctx.query(LightComponent, TagsComponent); // alle solchen Objekte
ctx.all();

Objekte erzeugen und entfernen​

ctx.create({ name: 'Bullet', parent, components: [] });
ctx.spawn(props.bulletModel, { parent });
ctx.destroy(entity);

spawn ist der einfache Weg. Geben Sie eine Ressource an, und es fügt die passenden Komponenten für Sie hinzu:

  • Ein Modell wird zu einem Modellobjekt.
  • Ein Bild wird zu einer flachen Ebene mit dem Bild darauf.
  • Ein Klang wird zu einer Audioquelle.

Eingebaute Schritte ausführen​

ctx.step('set_visibility', { visible: false }, { targets: [enemy] });

ctx.startTransition({ durationMs: 400, easing: 'ease-out' }, () => {
// Änderungen hier drin laufen fließend ab, statt zu springen
});

targets wählt die Objekte, auf die der Schritt wirkt, wie das Feld Zielobjekt eines Ereignisses. Ohne targets wirkt der Schritt auf das Objekt dieses Skripts.

Erst einen Schritt nutzen, dann selbst schreiben

ctx.step führt jeden der eingebauten Schritte aus, dieselben, die auch Ihre Ereignisse nutzen. Animationen, Zustandswechsel und Szenenübergänge sind nur einen Aufruf entfernt.

Schicken Sie den Besucher in eine andere Szene oder in einen anderen Raum:

ctx.openScene(sceneOrId);
await ctx.openSpace(spaceRefOrId);
ctx.scenes();

Eingabe​

Es gibt zwei Wege, Eingaben zu lesen.

Benannte Aktionen nutzen die Tastenbelegung des Projekts. Ändert ein Besucher seine Tasten, funktioniert Ihr Skript trotzdem:

ctx.input.pressed('jump');
ctx.input.justPressed('fire');
ctx.input.axis('moveX');

Rohe Tasten lesen die Tastatur direkt. Ein Tastendruck gilt genau einen Frame lang, lesen Sie ihn also innerhalb von ctx.tick:

ctx.keyboard.down('KeyW');
ctx.keyboard.press('Space');
ctx.keyboard.press('ArrowLeft', { every: 200 }); // automatisch wiederholen, alle 200 ms
ctx.keyboard.axis('KeyA', 'KeyD'); // -1, 0 oder 1
Modifikatortasten verhalten sich jeweils anders

Eine benannte Aktion ignoriert Modifikatoren, die sie nicht nennt. Wer Shift zum Sprinten hält, blockiert also „vorwärts“ nicht. Bei einem Tasten-Trigger ist es umgekehrt: Ein Modifikator, der nicht zu seiner Taste gehört, darf nicht gedrückt sein.

Verliert das Fenster den Fokus, gelten alle gehaltenen Tasten als losgelassen. So bleibt keine Taste hängen.

Kamera​

ctx.camera.entity();           // das aktive Kameraobjekt
ctx.camera.setActive(target); // Kamera wechseln; null kehrt zur Standardkamera zurück
ctx.camera.pose(); // { position, rotation, forward } in Weltkoordinaten

Die Transformation der Kamera funktioniert in jedem Steuerungsmodus gleich. Schreiben Sie hinein, um die Kamera zu bewegen. Lesen Sie sie, um zu sehen, wohin die Steuerung die Kamera gesetzt hat.

In den Modi orbit und firstPerson setzt die Steuerung die Rotation, eine geschriebene Rotation wird also ersetzt. Die Position, die Sie schreiben, bleibt erhalten.

Raycasting​

Ein Strahl ist eine Linie, die von einem Punkt in eine Richtung verläuft. Senden Sie einen aus, um zu finden, was auf ihm liegt.

await ctx.raycast();                                       // aus der Bildmitte der Kamera
await ctx.raycast({ screen: { x: 0.5, y: 0 }, all: true }); // ein Punkt auf dem Bildschirm, alle Treffer
await ctx.raycast({ origin, direction }); // ein beliebiger Strahl
await ctx.raycast({ from: entity }); // von einem Objekt aus, in seine Vorwärtsrichtung

Jeder Treffer liefert das Objekt, den Abstand sowie Punkt und Flächennormale in Weltkoordinaten. Die Treffer sind nach Nähe sortiert, einer pro Objekt.

await verwenden

Die Antwort kommt erst im nächsten Frame.

Unsichtbare Objekte werden übersprungen, ebenso Objekte, die als ignoriert markiert sind, etwa Zielmarkierungen und Gizmos. Sie bekommen dann das, was dahinter liegt, statt ins Leere zu treffen.

Physik​

ctx.physics.applyImpulse(target, { x: 0, y: 5, z: 0 });
ctx.physics.applyForce(target, vec, point);
ctx.physics.setVelocity(target, vec);
ctx.physics.teleport(target, position, { rotation, keepVelocity });
ctx.physics.setGravity(vec);

ctx.physics.getSpeed(target);
ctx.physics.isSleeping(target);

await ctx.physics.raycast(from, to, { skip: [ctx.entity] });
Die Transformation eines dynamischen Körpers zu ändern bewirkt nichts

Wo er ist, bestimmt die Physik. Setzen Sie ihn mit teleport an eine Stelle, und bewegen Sie ihn mit Impulsen oder Kräften.

Den eigenen Körper überspringen

Wenn Sie einen Strahl aus Ihrem eigenen Körper heraus senden, übergeben Sie skip. Sonst trifft der Strahl jedes Mal Ihren eigenen Körper.

Die Messwerte hinken etwa einen Frame hinterher. Für „Bewege ich mich?“ reicht das, für genaue Berechnungen nicht.

Audio​

ctx.audio.play(props.hitSound.id, { at: enemy, volume: 0.6, positional: true });

play erwartet die ID des Klangs. Bei einer Klang-Einstellung aus defineProps ist das props.hitSound.id.

Jeder Aufruf spielt einen eigenen Klang ab. Schritte, Treffer und Schüsse schneiden sich also nie gegenseitig ab. Die Audio-Komponente spielt immer nur einen Klang und bricht sich dabei selbst ab. Nutzen Sie sie deshalb nicht für Effekte.

Werte aufbewahren und teilen​

Es gibt drei Wege, Werte aufzubewahren und mit anderen Skripten zu teilen:

// 1. die eigenen Werte dieses Skripts
const store = ctx.store('game', { score: { type: 'number', default: 0 } });
store.set('score', (v) => v + 1);
store.subscribe('score', (v) => {});

// 2. globale Werte — geteilt mit jedem Skript, Patch und Ereignis
ctx.setGlobal('level', 3);
ctx.getGlobal('level');
ctx.subscribeGlobal('level', (v) => {});

// 3. Nachrichten zwischen Skripten
ctx.postMessage('enemy-died', { id });
ctx.handleMessage('enemy-died', ({ id }) => {});
Gespeichert inNach einem SzenenwechselNach dem Wechsel in einen anderen RaumNach dem Neuladen
storejaneinnein
globale Wertejajanein
globale Werte pro Raumjanein — jeder Raum hat absichtlich seine eigenennein
Wer den Tab schließt, verliert alles

Nichts davon wird zwischen zwei Besuchen gespeichert. Wenn Sie einen Wert beim nächsten Mal brauchen, schicken Sie ihn selbst irgendwohin, solange der Besucher noch verbunden ist.

Plugins​

Lesen und ändern Sie die eigene Komponente eines Plugins, und nutzen Sie die Trigger und Schritte, die ein Plugin hinzufügt:

const health = ctx.plugin('acme.health/health');   // '<plugin id>/<component id>'
for (const e of health.entities()) {
const max = health.props(e)!.max as number;
health.setState(e, { hp: max });
}

ctx.on('acme.patrol:reached', ({ point }) => {}); // ein Trigger des Plugins
ctx.step('acme.patrol:pause', { seconds: 2 }); // ein Schritt des Plugins

ctx.plugin hat entities(), has(e), props(e), state(e), setState, setProps, setEnabled, add und remove. Die Typen kommen vom Plugin, Ihr Code-Editor kennt also die Felder. Siehe Laufzeitcode.

Oberfläche​

Lesen und ändern Sie Werte auf einer UI-Karte und reagieren Sie auf ihre Schaltflächen:

const ui = ctx.getDivKit(entity);

ui.get('score');
ui.set('score', (v) => v + 1);
ui.subscribe('lives', (v) => {});
ui.onAction('restart', () => {});

Siehe UI-Karten.


Weiter: Rezepte — vollständige Skripte zum Kopieren.