Перейти к основному содержимому

API ctx

ctx — ваша ручка к работающей сцене: он передаётся в init, и всё нижеследующее висит на нём.

Он намеренно небольшой. Если вы что-то ищете и не находите здесь, есть немалый шанс, что ответ — это встроенный шаг, а не API.

Где вы находитесь

ctx.entityобъект, к которому прикреплён этот скрипт
ctx.sceneсцена, которой он принадлежит
ctx.spaceмир

Жизненный цикл и события

ctx.tick((dt, t) => {});          // каждый кадр; dt и t в секундах
ctx.effect(() => {}); // перезапускается при изменении прочитанного; может вернуть очистку
ctx.on(trigger, (payload) => {}); // подписаться на триггер
ctx.emit(trigger, payload); // поднять триггер с этого объекта

Триггеры называются так же, как у событий и патчей. Вот те, что несут полезную информацию:

ТриггерЧто вы получаете
on-keydown · on-keyup{ code, ctrl, shift, alt, meta }
on-state-active · on-state-inactive{ stateId }
on-collide{ other } — во что врезались
on-divkit-action{ id, … } — какая кнопка
on-game-control{ state } — простой, движение, бег или прыжок
on-drag · on-pinch · on-rotate{ dx, dy } · { scale } · { angle }
on-vps-localizedгде в итоге оказался посетитель

on-launch приходит в момент создания вашего экземпляра, так что пропустить его, запустившись поздно, невозможно.

Поиск объектов

ctx.get(id);
ctx.findByName('Door');
ctx.find(LightComponent); // первый объект с этими компонентами
ctx.query(LightComponent, TagsComponent); // все такие
ctx.all();

Создание и удаление

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

spawn — удобный вариант: передайте ему ресурс, и он соберёт подходящие компоненты. Модель станет объектом-моделью, изображение — плоскостью с текстурой, звук — источником звука.

Запуск встроенного поведения

ctx.step('play_animation', { presetId }, { targets: [enemy] });

ctx.startTransition({ durationMs: 400, easing: 'ease-out' }, () => {
// изменения, сделанные здесь, едут плавно, а не щёлкают
});
Это тот сокращённый путь, который чаще всего упускают

ctx.step выполняет любой из встроенных шагов — тех же, что используют ваши события. Анимация, переключение состояний, переходы между сценами и транзишены — в одном вызове, так что переписывать их почти никогда не нужно.

Навигация

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

Ввод

Два слоя для двух разных задач.

Именованные действия читают раскладку проекта, так что посетитель, переназначивший клавиши, получает уважение к своему выбору:

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

Сырые клавиши читают клавиатуру напрямую. Нажатие длится ровно один кадр, поэтому читать их надо внутри ctx.tick:

ctx.keyboard.down('KeyW');
ctx.keyboard.press('Space');
ctx.keyboard.press('ArrowLeft', { every: 200 }); // автоповтор, в мс
ctx.keyboard.axis('KeyA', 'KeyD'); // -1, 0 или 1
Эти два по-разному обращаются с модификаторами, и это намеренно

Именованная привязка игнорирует модификаторы, которых не упоминает: бег с зажатым Shift не должен отменять «вперёд». Клавиатурный триггер устроен наоборот: модификатор, который вы не отметили, означает «не должен быть зажат».

Потеря фокуса окном сбрасывает зажатые клавиши, так что ничего не залипает.

Камера

ctx.camera.entity();           // объект активной камеры
ctx.camera.setActive(target); // переключить камеру; null возвращает камеру по умолчанию
ctx.camera.pose(); // { position, rotation, forward } в мировых координатах

Transform камеры — источник правды в любом режиме управления: пишите в него, чтобы её подвинуть, читайте, чтобы увидеть, куда её поставило управление. В режимах orbit и first-person ориентацией владеет управление, так что записанный вами поворот будет перезаписан, — позиция уважается.

Raycast

await ctx.raycast();                                       // из центра камеры
await ctx.raycast({ screen: { x: 0.5, y: 0 }, all: true }); // экранная точка, все попадания
await ctx.raycast({ origin, direction }); // любой луч на ваш вкус
await ctx.raycast({ from: entity }); // от объекта, вдоль его «вперёд»

Каждое попадание сообщает объект, дистанцию, а также точку и нормаль поверхности в мировых координатах — отсортированные от ближайшего, по одному попаданию на объект.

Он возвращает промис, и это не ошибка

Луч бросается по реальной геометрии на стороне рендеринга, поэтому ответ приходит следующим кадром. Невидимые объекты и помеченные как игнорируемые (прицелы, гизмо) пропускаются, так что отвечает то, что за ними, а не луч, сообщающий о промахе.

Физика

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] });
Установка transform у динамического тела ничего не делает

Физика владеет его позицией. Ставьте через teleport, двигайте импульсами или силами.

И передавайте skip, когда пускаете луч изнутри собственного тела, иначе будете попадать в себя каждый раз.

Показания берутся из последнего синхронизированного состояния и отстают примерно на кадр — нормально для «я двигаюсь?», неправильно для точной мгновенной математики.

Звук

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

Каждый вызов запускает независимый звук — именно это и нужно шагам, ударам и стрельбе. Компонент audio — один голос, и он будет обрывать сам себя; для эффектов его не используйте.

Как что-то хранить

Три хранилища, отличающиеся дальностью:

// 1. собственные значения этого скрипта
const store = ctx.store('game', { score: { type: 'number', default: 0 } });
store.set('score', (v) => v + 1);
store.subscribe('score', (v) => {});

// 2. глобальные значения — общие со всеми скриптами, патчами и событиями
ctx.setGlobal('level', 3);
ctx.getGlobal('level');
ctx.subscribeGlobal('level', (v) => {});

// 3. сообщения между скриптами
ctx.postMessage('enemy-died', { id });
ctx.handleMessage('enemy-died', ({ id }) => {});
Переживает смену сценыПереживает переход между spacesПереживает перезагрузку
storeданетнет
глобальные значениядаданет
глобальные в области spaceданет — намеренно изолированынет
Ничто из этого не переживает закрытие вкладки

Ни одно из трёх не сохраняется между визитами. Если что-то должно сохраниться, отправьте его куда-то сами, пока соединение ещё есть.

Интерфейс

const ui = ctx.getDivKit(entity);

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

См. Карточки интерфейса.


Дальше: То, что вы действительно будете собирать — готовые скрипты, которые можно скопировать.