Aller au contenu principal

Ce que vous construirez vraiment

Chaque script ici est complet. Copiez-en un, attachez-le à un objet, changez les nombres.

Si vous n'en avez jamais écrit, commencez par Votre premier script — il explique la forme qu'ils partagent tous.

Faire tourner quelque chose indéfiniment

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; // degrés → radians
t.update({
rotation: { ...t.$data.rotation, y: t.$data.rotation.y + turn * dt },
});
});
});
Pourquoi multiplier par dt

dt est la durée de la dernière image. Multiplier par lui signifie que l'objet tourne d'autant par seconde sur un téléphone rapide comme sur un lent. Retirez-le et votre animation tourne à la vitesse que l'appareil parvient à tenir.

Faire osciller doucement quelque chose

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 oscille doucement entre −1 et 1, indéfiniment. Multipliez-le pour régler l'amplitude, multipliez le temps pour régler la vitesse.

Toucher pour ramasser

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);
});
});

Le score vit dans un global pour que tout le reste (un compteur à l'écran, la condition de victoire) puisse le lire.

Afficher ce score à l'écran

Posez ceci sur l'objet qui porte votre carte d'interface :

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); // afficher la valeur de départ
});
C'est le schéma à retenir

Votre logique possède les nombres ; la carte se contente de les afficher. Vouloir garder le score dans la carte s'écroule dès que quoi que ce soit d'autre en a besoin.

Un compte à rebours

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)}`);
});
});

Faire quelque chose toutes les quelques secondes

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 });
});
});

Il n'y a pas de setInterval ici. Compter les secondes dans tick en est l'équivalent, et cela s'arrête tout seul quand l'objet disparaît.

Faire apparaître quelque chose qui a son propre comportement

Celui-ci prend les gens au dépourvu. ctx.spawn construit un objet à partir d'une ressource : faire apparaître un modèle vous donne donc un modèle — et rien d'autre. Il ne transporte pas de script.

Pour un ennemi qui poursuit, une balle qui vole, un objet à ramasser qui réagit, construisez l'objet vous-même et attachez-lui le comportement :

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));
});

Chaque objet créé reçoit sa propre instance de ce script, avec ses propres variables — donc les points de vie, l'état et les minuteries par ennemi ne demandent rien de spécial :

// le script propre à l'ennemi
init((ctx) => {
let health = 3;

ctx.on('on-collide', () => {
health -= 1;
if (health <= 0) ctx.destroy(ctx.entity);
});
});
Donnez-lui aussi un collider si la physique doit le voir

Un objet construit avec create est exactement les composants que vous avez listés. Oubliez le collider et il n'entrera pas en collision ; oubliez le corps rigide et la physique ne le déplacera pas.

Trouver le plus proche

L'autre moitié d'une tourelle, d'un ennemi, ou de tout ce qui vise :

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;
};

Obtenez les candidats avec ctx.query(...) — demandez un composant que seuls les ennemis portent, ou marquez-les avec des tags à la création et filtrez là-dessus.

Suivre le joueur, mais pas de trop près

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; // assez près

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 },
});
});
});

Diviser par gap transforme la direction en une longueur de exactement 1 : multiplier par step déplace donc précisément de cette distance. Cette astuce revient sans arrêt.

Tirer sur ce que vous regardez

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 est nécessaire parce que le rayon est tracé contre la vraie scène et que la réponse revient à l'image suivante.

Lancer un objet physique

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,
});
});
});
Placez-le avec teleport, poussez-le avec applyImpulse

Fixer directement la position d'un objet physique ne fait rien — la physique possède sa position et vous écrase au pas suivant.

Une porte qui s'ouvre une fois

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 est ce qui fait glisser le changement au lieu de le faire claquer. Tout ce que vous modifiez à l'intérieur s'interpole ensemble.

Réagir à une collision

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 prévient les autres scripts sans qu'aucun n'ait besoin d'une référence vers l'autre.

Attendre que la scène soit suivie

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

ctx.on('on-lost', () => {
ctx.step('stop_animation', { presetId: 'intro' });
});
});
ctx.step vous évite de réécrire le moteur

Tout ce que l'éditeur sait faire sous forme d'étape (animations, transitions, states, navigation), un script peut le déclencher en un appel. Parcourez la référence des étapes avant de construire quelque chose à la main.

Un configurateur de produit

La construction commerciale la plus courante sur cette plateforme, et c'est essentiellement deux gestes : changer ce qui est montré, et retenir ce qui a été choisi.

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;

// Un slot de matériau peut pointer vers un matériau enregistré au lieu de porter des valeurs.
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); // démarrer sur la première finition
});

Échanger un modèle entier plutôt qu'une finition suit la même forme, en écrivant referal sur le composant de modèle :

ctx.entity.getComponent(ModelRefComponent)?.update({ referal: props.models[index]?.id ?? null });
Donnez-lui une caméra que le visiteur peut faire tourner, et pas d'AR

Un configurateur est généralement une scène 3D ordinaire : elle s'ouvre instantanément, fonctionne sur tous les appareils et ne demande aucun marqueur. Réglez la caméra sur orbit et limitez l'angle polaire pour que personne ne finisse par le regarder par en dessous. → Caméras

Activez la capture photo

Un configurateur qu'on peut prendre en photo est un configurateur qu'on partage. → Réglages du projet

Où mettre votre état

La valeur est…Mettez-la dans
utilisée par ce script seulementune simple variable dans init
nécessaire après un changement de scènectx.store
nécessaire à d'autres scripts, événements ou patchesctx.setGlobal
un événement auquel d'autres doivent réagirctx.postMessage
partagée avec d'autres personnes dans une sallectx.net.state
Rien de tout cela ne survit à la fermeture de l'onglet

Si quelque chose doit être là la prochaine fois, envoyez-le à votre propre serveur pendant que le visiteur est encore sur la page.


Suite : Multijoueur