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 },
});
});
});
dtdt 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
});
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);
});
});
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,
});
});
});
teleport, poussez-le avec applyImpulseFixer 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 moteurTout 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 });
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
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 seulement | une simple variable dans init |
| nécessaire après un changement de scène | ctx.store |
| nécessaire à d'autres scripts, événements ou patches | ctx.setGlobal |
| un événement auquel d'autres doivent réagir | ctx.postMessage |
| partagée avec d'autres personnes dans une salle | ctx.net.state |
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