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

API ctx

Через ctx скрипт работает с запущенной сценой. На этой странице собрано всё, что он умеет.

init передаёт ctx в ваш скрипт. API специально сделан небольшим. Если чего-то здесь нет, поищите встроенный шаг, который это делает.

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

ИмяЧто это
ctx.entityобъект, к которому прикреплён скрипт
ctx.sceneсцена, в которой находится объект
ctx.spaceпространство — весь мир, в котором находится посетитель

Кадры и триггеры​

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

Большинство триггеров называются так же, как в событиях. Три триггера ввода с ветками приходят в скрипт как отдельные триггеры:

  • Клавиша (on-key) приходит как on-keydown и on-keyup;
  • Нажатие на объект (on-mouse) приходит как on-mousedown, on-mouseup и on-click;
  • Наведение (on-hover) приходит как on-hover и on-blur.

Триггеры времени, расстояния, видимости, сигналов и переменных работают только в событиях. В скрипте вместо них используйте ctx.tick, ctx.subscribeGlobal и ctx.handleMessage.

Вот триггеры, которые передают полезные данные:

ТриггерЧто вы получаете
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('set_visibility', { visible: false }, { targets: [enemy] });

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

targets выбирает объекты, на которые действует шаг, — как поле Объект в событии. Без него шаг действует на объект этого скрипта.

Прежде чем писать самому, поищите шаг

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 }); // автоповтор каждые 200 мс
ctx.keyboard.axis('KeyA', 'KeyD'); // -1, 0 или 1
Клавиши-модификаторы работают по-разному

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

Когда окно теряет фокус, все зажатые клавиши отпускаются, так что ни одна не залипнет.

Камера​

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

Трансформация камеры работает одинаково во всех режимах управления. Запишите в неё значение, чтобы сдвинуть камеру. Прочитайте её, чтобы узнать, куда камеру поставило управление.

В режимах orbit и firstPerson поворот задаёт сам режим управления, поэтому записанный вами поворот будет заменён. А записанное положение сохранится.

Лучи​

Луч — это линия из точки в заданном направлении. Пустите луч, чтобы узнать, что лежит на его пути.

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

Каждое попадание даёт объект, расстояние, точку и нормаль поверхности в мировых координатах. Попадания отсортированы от ближнего к дальнему, по одному на объект.

Используйте await

Ответ приходит в следующем кадре.

Невидимые объекты пропускаются, как и объекты, помеченные как игнорируемые, например прицелы и гизмо. Вместо промаха вы получите то, что за ними.

Физика​

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] });
Менять трансформацию динамического тела бесполезно

Где оно находится, решает физика. Чтобы переставить тело, используйте teleport, а чтобы сдвинуть — импульсы или силы.

Пропускайте своё тело

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

Значения отстают примерно на кадр. Чтобы понять, «движусь ли я», этого хватает, а для точных расчётов — нет.

Звук​

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

play принимает id звука. Если звук задан настройкой из defineProps, это props.hitSound.id.

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

Как хранить значения и делиться ими​

Есть три способа хранить значения и делиться ими с другими скриптами:

// 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 }) => {});
Где хранитсяПосле смены сценыПосле перехода в другое пространствоПосле перезагрузки
storeданетнет
глобальные переменныедаданет
глобальные переменные пространстваданет — у каждого пространства свои, так задуманонет
Если закрыть вкладку, всё сотрётся

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

Плагины​

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

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 }) => {}); // триггер плагина
ctx.step('acme.patrol:pause', { seconds: 2 }); // шаг плагина

У ctx.plugin есть entities(), has(e), props(e), state(e), setState, setProps, setEnabled, add и remove. Типы берутся из плагина, поэтому редактор кода знает его поля. См. Код в проекте.

Интерфейс​

Читайте и меняйте значения в карточке интерфейса и реагируйте на её кнопки:

const ui = ctx.getDivKit(entity);

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

Подробнее — на странице Карточки интерфейса.


Дальше: Рецепты — готовые скрипты, которые можно скопировать.