import React from 'react'; import { Code, ScriptKind, Step, Note, Try, Shot } from './docsData'; /** * docsLessons.jsx — тексты уроков для 50 мини-игр (раздел K вики). * * LESSONS — объект, ключ = id игры из docsGames.js, значение — * { intro, mechanics, body } где body — JSX подробной инструкции. * Хелперы оформления (Code, ScriptKind, Step, Note, Try, Shot) общие * с docsData.jsx. * * Уроки наполняются по одному. Если урока для игры ещё нет — карточка * в каталоге показывает «Урок скоро». */ export const LESSONS = { // ════════════════════════════════════════════════════ // ИГРА 1 — «Собери монетки» // ════════════════════════════════════════════════════ 'collect-coins': { body: ( <>
Небольшая площадка, по которой раскиданы золотые монетки. Игрок ходит и собирает их — за каждую монетку счёт растёт на единицу. Когда собраны все 8 монеток — победа, летит конфетти.
onTouch);game.ui.score);game.self.delete);Сначала построим пол, по которому будет ходить игрок.
Монетка — это примитив-сфера жёлтого цвета. Поставим 8 штук.
#ffd700,
материал «Неон» — чтобы монетка светилась.
Главный скрипт считает монетки и проверяет победу.
{`// === ИГРА «СОБЕРИ МОНЕТКИ» — главный скрипт ===
// Этот скрипт глобальный: считает собранные монетки и проверяет победу.
let score = 0; // сколько монеток собрано
const TOTAL = 8; // всего монеток на уровне
game.ui.score = score; // показать счёт 0 в углу
game.ui.showText('Собери все монетки!', 2); // подсказка на старте
// Монетки сообщают сюда о сборе через game.broadcast('coin').
// Каждый скрипт работает в своей «песочнице» — переменные одного
// скрипта не видны другому. Поэтому скрипты общаются сообщениями:
// один шлёт game.broadcast('имя'), другой ловит game.onMessage('имя').
game.onMessage('coin', () => {
score = score + 1; // +1 к счёту
game.ui.score = score; // обновить число на экране
game.sound.play('coin'); // звон монетки
if (score >= TOTAL) {
// собраны все — победа!
game.ui.showText('Победа! Все монетки твои!', 4);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z },
{ duration: 3, count: 3 });
}
});`}
Разберём построчно:
let score = 0 — переменная-счётчик,
в начале монеток собрано 0;game.ui.score = score — выводит счёт
в угол экрана;game.onMessage('coin', ...) — подписка
на сообщение «coin». Каждый раз, когда монетка
пришлёт это сообщение, выполнится наша функция —
прибавит счёт;if (score {'>'}= TOTAL) — когда счёт дошёл
до 8, показываем победу и запускаем конфетти.Теперь повесим скрипт на каждую монетку. Он ловит касание и сообщает главному скрипту: меня собрали.
{`// === Скрипт монетки ===
// game.self — это сама монетка, на которой висит скрипт.
game.self.onTouch(() => {
// игрок коснулся монетки — сообщаем главному скрипту
game.broadcast('coin');
game.self.delete(); // монетка исчезает со сцены
});`}
Что происходит: onTouch срабатывает, когда
игрок дотронулся до монетки. Внутри мы шлём
game.broadcast('coin') — главный скрипт
ловит это сообщение и прибавляет очко, а
game.self.delete() убирает монетку.
Нажми Запустить. Походи по площадке и собери монетки:
Если что-то не работает — открой Консоль внизу справа, там будут ошибки скриптов.
game.onTick поворачивай все монетки, чтобы
они крутились и были заметнее. Поставь больше монеток.
Сделай одну монетку красной и «секретной» — за неё
давай сразу +5 очков.
Паркур: в воздухе висит дорожка из платформ с разрывами. Игрок прыгает с платформы на платформу и должен добраться до зелёной финишной площадки. Если упал вниз — игра возвращает на старт.
onTick +
game.player.respawn);Главный скрипт следит за падением и обрабатывает победу.
{`// === ИГРА «ПРЫГАЙ ПО ПЛАТФОРМАМ» — главный скрипт ===
let won = false; // победа уже была?
game.ui.showText('Допрыгай до зелёной площадки!', 3);
// Каждый кадр проверяем высоту игрока.
game.onTick(() => {
if (won) return;
const p = game.player.position;
// упал ниже всех платформ — вернуть на старт
if (p && p.y < -3) {
game.player.respawn();
game.ui.showText('Упал! Пробуй снова.', 1.5);
game.sound.play('lose');
}
});
// Финиш сообщает сюда о победе через broadcast/onMessage:
// скрипты живут в РАЗНЫХ песочницах, общая переменная между ними
// не видна — связь только сообщениями.
game.onMessage('finish', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты дошёл до финиша!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z },
{ duration: 3, count: 3 });
});`}
Что тут важно:
game.onTick(...) — функция внутри
выполняется каждый кадр. Так мы постоянно
следим за игроком;if (p.y {'<'} -3) — если игрок опустился
ниже -3 по высоте, значит свалился с платформ;game.player.respawn() — возвращает игрока
на точку спавна;won — флажок, чтобы победа сработала
только один раз.{`// === Скрипт финиша ===
// Висит на невидимой зоне над зелёной площадкой.
// Игрок встал на площадку — его тело внутри зоны — победа.
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о победе
});`}
Когда игрок касается финиша, скрипт шлёт
game.broadcast('finish'). Главный скрипт ловит
это сообщение через game.onMessage('finish', ...) —
там показывается «Победа» и летит конфетти. Скрипты в разных
«песочницах» общаются только сообщениями.
game.tween с
yoyo: true. Сделай платформы поменьше —
прыгать будет сложнее. Поставь на пути монетки из урока 1.
Дорожка из жёлтых плиток. Как только игрок встаёт на плитку, она через секунду исчезает. Стоять нельзя — нужно всё время бежать вперёд, к зелёному финишу. Зазевался — плитка пропала, и ты падаешь вниз.
game.after — как выполнить
что-то не сразу, а через несколько секунд;Следит за падением и победой — как в уроке 2.
{`// === ИГРА «НЕ УПАДИ» — главный скрипт ===
let won = false;
game.ui.showText('Беги вперёд! Плитки исчезают!', 3);
game.onTick(() => {
if (won) return;
// упал вниз — на старт
const p = game.player.position;
if (p && p.y < -3) {
game.player.respawn();
game.ui.showText('Упал! Снова.', 1.5);
game.sound.play('lose');
}
});
// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('finish', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты добежал!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Самое главное — повесить на каждую плитку скрипт, который убирает её через секунду после касания.
{`// === Скрипт исчезающей плитки ===
let triggered = false; // плитка уже запущена на исчезновение?
game.self.onTouch(() => {
if (triggered) return; // если уже запущена — выходим
triggered = true;
game.sound.play('click');
// через 1.2 секунды плитка пропадает
game.after(1.2, () => {
game.self.delete();
});
});`}
Разберём:
triggered — флажок-защёлка. Игрок может
коснуться плитки несколько раз, но запустить таймер
нужно только один раз — флажок это гарантирует;game.after(1.2, () => {'{...}'}) —
«через 1.2 секунды выполни это». Внутри —
game.self.delete(), плитка исчезает;1.2 на другое число —
меньше значит сложнее, плитки пропадают быстрее.game.sound.play('click') при касании —
подсказка игроку: «плитка пошла исчезать, беги!».
Комната перегорожена стеной с дверью. Игрок подходит к красной кнопке, нажимает клавишу E — и дверь плавно уезжает вверх, открывая проход к финишу.
game.self.onInteract);game.scene.findOne;game.tween).{`// === ИГРА «КНОПКА-ОТКРЫВАШКА» — главный скрипт ===
game.ui.showText('Подойди к красной кнопке и нажми E', 4);
// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
game.ui.showText('Победа! Дверь открыта, ты прошёл!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Скрипт кнопки делает всю работу: ловит нажатие E и открывает дверь.
{`// === Скрипт кнопки ===
let opened = false;
game.self.onInteract(() => {
if (opened) return; // дверь уже открыта
opened = true;
game.sound.play('click');
// находим дверь по имени
const door = game.scene.findOne('Дверь');
// плавно поднимаем её вверх на высоту 8
game.tween(door, { y: 8 }, { duration: 1.2, easing: 'ease' });
game.ui.showText('Дверь открывается!', 2);
}, {
text: 'Открыть дверь', // подсказка над кнопкой
distance: 4 // на сколько метров подойти
});`}
Разберём:
game.self.onInteract(fn, опции) — это
и есть взаимодействие по E. Когда игрок подходит ближе
чем на distance метров, над кнопкой
появляется подсказка «Открыть дверь». Нажатие
E запускает функцию;game.scene.findOne('Дверь') — находит дверь
по имени, которое мы задали в инспекторе;game.tween(door, {'{ y: 8 }'}, ...) —
плавно меняет высоту двери с текущей до 8, за 1.2
секунды. Дверь уезжает вверх — проход открыт.{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}
Запусти игру:
Лабиринт из стен. Игрок появляется в одном углу и должен найти путь к зелёному выходу в другом конце. Стены высокие — через них не перепрыгнуть, нужно искать проход.
Прежде чем строить — нарисуй лабиринт на листе в клетку. Клетка = проход или стена. Отметь, где старт, где выход. Обязательно проверь карандашом, что от старта до выхода есть путь — иначе игру не пройти.
Скрипты совсем простые — лабиринт держится на постройке.
{`// === ИГРА «ЛАБИРИНТ» — главный скрипт ===
game.ui.showText('Найди выход из лабиринта!', 3);
// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
game.ui.showText('Победа! Ты нашёл выход!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}
На полу — сетка серых плиток. Когда игрок наступает на плитку, она становится ярко-зелёной. Цель — обойти и раскрасить все плитки.
game.scene.setColor;{`// === ИГРА «ЦВЕТНЫЕ ПЛИТКИ» — главный скрипт ===
let painted = 0; // сколько плиток раскрашено
const TOTAL = 36; // всего плиток (6×6)
game.ui.score = 0;
game.ui.showText('Наступи на все плитки!', 3);
// Плитки сообщают о покраске через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('paint', () => {
painted = painted + 1;
game.ui.score = painted;
game.sound.play('pickup');
if (painted >= TOTAL) {
game.ui.showText('Победа! Все плитки раскрашены!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});`}
36 на столько плиток, сколько
реально поставил. Если сетка 5×5 — будет 25.
{`// === Скрипт цветной плитки ===
let painted = false; // плитка уже раскрашена?
game.self.onTouch(() => {
if (painted) return;
painted = true;
// меняем цвет плитки на ярко-зелёный
game.scene.setColor(game.self.ref, '#33dd55');
game.broadcast('paint'); // сообщаем главному скрипту о покраске
});`}
Главное здесь:
game.scene.setColor(ref, цвет) — меняет
цвет объекта прямо во время игры;game.self.ref — «адрес» этой плитки.
Любая команда game.scene.* работает
по ref, и через game.self.ref скрипт
передаёт адрес своего объекта;game.broadcast('paint') — шлёт сообщение
«paint». Главный скрипт ловит его через
game.onMessage('paint', ...) и прибавляет
счёт. Скрипты живут в разных «песочницах» — переменные
одного не видны другому, общаются только сообщениями;painted — чтобы плитка
засчиталась только один раз.game.random. Или сделай «вредные»
красные плитки: наступил — счёт обнулился, начинай заново.
С неба каждые полторы секунды падает жёлтый куб в случайном месте. Игрок бегает по площадке и ловит кубы — касаешься куба, он засчитывается и исчезает. Поймай 15 кубов.
game.scene.spawn);game.every — делать что-то
снова и снова с интервалом;game.random;Кубы создаются скриптом, заранее их ставить не нужно — вся игра в одном скрипте.
{`// === ИГРА «ПОЙМАЙ ПАДАЮЩЕЕ» — главный скрипт ===
let score = 0;
const GOAL = 15; // сколько кубов нужно поймать
let won = false;
// Пойманные кубы — чтобы не засчитать один куб дважды.
const caught = {};
game.ui.score = 0;
game.ui.showText('Лови падающие кубы! Нужно 15', 3);
// Каждые 1.5 секунды роняем с неба новый куб.
game.every(1.5, () => {
if (won) return;
const x = game.random(-6, 6); // случайная точка над площадкой
const z = game.random(-6, 6);
const cube = game.scene.spawn('primitive:cube', {
x: x, y: 14, z: z,
sx: 0.8, sy: 0.8, sz: 0.8,
color: '#ffcc33',
anchored: false, // anchored:false — куб ПАДАЕТ (физика)
});
// если за 6 секунд не поймали — куб исчезнет сам
game.scene.deleteAfter(cube, 6);
});
// Игрок коснулся падающего куба. onPlayerTouch шлёт e.target —
// ref ('primitive:N') только для заспавненных объектов (кубов),
// пол и стены сюда НЕ приходят.
game.onPlayerTouch((e) => {
if (won) return;
const ref = e && e.target;
if (!ref || caught[ref]) return; // нет ref или куб уже пойман
caught[ref] = true;
score = score + 1;
game.ui.score = score;
game.sound.play('coin');
game.scene.delete(ref); // пойманный куб исчезает
if (score >= GOAL) {
won = true;
game.ui.showText('Победа! Ты поймал 15 кубов!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});`}
Разберём по частям:
game.every(1.5, fn) — каждые 1.5 секунды
выполняет функцию. В ней мы создаём куб;game.scene.spawn('primitive:cube', опции) —
создаёт куб прямо во время игры. Высота
y: 14 — куб появляется высоко, а
anchored: false — он падает по физике;game.random(-6, 6) — случайное число
от -6 до 6, так куб падает в случайном месте;caught[ref] = true — помечаем уже пойманный
куб, чтобы одно касание не засчиталось дважды;deleteAfter(cube, 6) — если куб не поймали
за 6 секунд, он сам исчезнет (уже упал на землю).caught? Касание куба может прийти
несколько раз подряд. Пометка caught[ref]
гарантирует: каждый куб засчитываем ровно один раз.
Длинная беговая трасса. Как только начинается игра — включается секундомер. Игрок бежит к зелёному финишу. Добежал — секундомер останавливается и показывает результат. Можно соревноваться: кто быстрее.
game.ui.timer;{`// === ИГРА «БЕГИ К ФИНИШУ» — главный скрипт ===
let finished = false;
let time = 0; // прошло секунд
game.ui.timer = 0;
game.ui.showText('Беги к зелёному финишу — на время!', 3);
// Каждый кадр прибавляем к таймеру прошедшее время.
game.onTick((dt) => {
if (finished) return;
time = time + dt; // dt — секунды с прошлого кадра
game.ui.timer = time; // показываем секундомер
});
// Финиш сообщает о завершении забега через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('finish', () => {
if (finished) return;
finished = true;
// округляем время до десятых
const t = Math.round(time * 10) / 10;
game.ui.showText('Финиш! Твоё время: ' + t + ' сек', 6);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Главное здесь — измерение времени:
game.onTick((dt) => {'{...}'}) —
функция получает dt: сколько секунд прошло
с прошлого кадра (обычно ~0.016 — это 1/60 секунды);time = time + dt — складывая все dt,
мы получаем, сколько всего прошло времени;game.ui.timer = time — выводит секундомер
на экран в формате ММ:СС;Math.round(time * 10) / 10 — округляет
до одной цифры после запятой (12.3 вместо 12.34567).{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о финише
});`}
game.save — рекорд.
В конце дорожки — большой шар-светофор. Он по очереди становится зелёным и красным. На зелёный можно бежать к финишу, на красный нужно замереть. Двинулся на красный — игра возвращает на старт.
game.after;Это самый сложный скрипт пока — разберём внимательно.
{`// === ИГРА «СВЕТОФОР» — главный скрипт ===
let phase = 'green'; // 'green' (беги) или 'red' (замри)
let won = false;
let prev = null; // прошлая позиция игрока
game.ui.showText('Зелёный — беги! Красный — замри!', 3);
// findOne нельзя сразу в начале скрипта — снимок сцены приходит
// чуть позже. Светофор находим и запускаем цикл через game.after.
let light = null;
// Переключаем свет: зелёный 3с, красный 2.5с, по кругу.
function green() {
if (won || !light) return;
phase = 'green';
game.scene.setColor(light, '#22dd55');
game.ui.showText('ЗЕЛЁНЫЙ — беги!', 1.2);
game.after(3, red);
}
function red() {
if (won || !light) return;
phase = 'red';
game.scene.setColor(light, '#e23b3b');
game.ui.showText('КРАСНЫЙ — замри!', 1.2);
game.after(2.5, green);
}
game.after(0.2, () => {
light = game.scene.findOne('Светофор');
green(); // начинаем с зелёного
});
// Каждый кадр: если красный и игрок шевелится — на старт.
game.onTick((dt) => {
if (won) return;
const p = game.player.position;
if (!p) return;
if (prev && phase === 'red') {
// на сколько игрок сдвинулся с прошлого кадра
const moved = Math.hypot(p.x - prev.x, p.z - prev.z);
if (moved / dt > 0.8) { // двигался — пойман
game.player.respawn();
game.ui.showText('Двинулся на красный! На старт.', 2);
game.sound.play('lose');
}
}
prev = { x: p.x, z: p.z }; // запомнить позицию
});
// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты дошёл до финиша!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Как работают фазы:
green() ставит зелёный цвет и через
game.after(3, red) запланирует
переключение на красный через 3 секунды;red() — наоборот: красный, и через 2.5с
обратно green. Так свет мигает по кругу;Как ловится движение:
prev;Math.hypot(dx, dz) — на сколько игрок
сдвинулся за кадр;moved / dt — это его скорость.
Если на красный скорость больше 0.8 — игрок шевелится,
возвращаем на старт.{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}
Башня из площадок-этажей на разной высоте. Между ними — оранжевые батуты. Игрок встаёт на батут, тот подбрасывает его высоко вверх — на следующий этаж. Допрыгай до верха.
{`// === ИГРА «ПРЫЖОК-ПРУЖИНА» — главный скрипт ===
let won = false;
game.ui.showText('Прыгай по батутам всё выше!', 3);
// упал вниз — на старт
game.onTick(() => {
if (won) return;
const p = game.player.position;
if (p && p.y < -3) {
game.player.respawn();
game.sound.play('lose');
}
});
// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты допрыгал до верха!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Повесь этот скрипт на каждый батут — он одинаковый.
{`// === Скрипт батута ===
// Игрок встал на батут — мощный подброс вверх.
game.self.onTouch(() => {
game.player.boostJump(3.2); // 3.2 = в 3 раза выше обычного прыжка
game.sound.play('jump');
});`}
game.player.boostJump(сила) — мгновенно
подбрасывает игрока. 1 — как обычный прыжок,
3.2 — в три с лишним раза выше. Подбери число
так, чтобы игрок допрыгивал от батута до следующего этажа.
boostJump(4)) или поставь этажи ближе.
Если перелетает — наоборот, уменьши.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}
Комната с шестью разноцветными плитками. Когда игрок наступает на плитку — она звучит и вспыхивает искрами. Каждая плитка — свой звук. Пройди все шесть, потом встань на зелёный финиш в центре.
game.sound.play;{`// === ИГРА «ЭХО-КОМНАТА» — главный скрипт ===
let stepped = 0; // на сколько плиток наступили
const TOTAL = 6;
game.ui.score = 0;
game.ui.showText('Наступи на все цветные плитки!', 3);
// Плитки и финиш сообщают сюда через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
// игрок впервые наступил на звуковую плитку
game.onMessage('step', () => {
stepped = stepped + 1;
game.ui.score = stepped;
if (stepped >= TOTAL) {
game.ui.showText('Все плитки звучали! Иди на финиш.', 3);
}
});
// игрок встал на финиш
game.onMessage('finish', () => {
if (stepped < TOTAL) {
game.ui.showText('Сначала пройди все ' + TOTAL + ' плиток!', 2);
return;
}
game.ui.showText('Победа! Эхо-комната пройдена!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Повесь на каждую плитку. Поменяй звук — у каждой плитки
свой: 'coin', 'jump',
'pickup', 'click',
'hit'.
{`// === Скрипт звуковой плитки ===
let used = false; // на эту плитку уже наступали?
game.self.onTouch(() => {
// звук играет КАЖДЫЙ раз — это эхо-комната
game.sound.play('coin'); // у каждой плитки свой звук!
// вспышка частиц над плиткой
game.scene.spawnParticles('sparks', game.self.position,
{ duration: 0.6, color: '#e23b3b' });
// засчитываем плитку только в первый раз
if (!used) {
used = true;
game.broadcast('step'); // сообщаем главному скрипту о новой плитке
}
});`}
Разберём:
game.sound.play('coin') — проигрывает
звук. Доступные звуки: coin, jump,
pickup, click, hit,
win, lose;game.self.position — координаты самой
плитки, над ней появятся искры;game.broadcast('step') — шлёт сообщение
главному скрипту, тот ловит его через
game.onMessage('step', ...). Скрипты
в разных «песочницах» общаются только сообщениями;used.{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish'); // сообщаем главному скрипту о финише
});`}
Перед запертой дверью — четыре кнопки с цифрами. Чтобы дверь открылась, нужно нажать кнопки (клавишей E) в правильном порядке — знать секретный код. Ошибся — код сбрасывается, начинай заново.
Здесь самое интересное — проверка кода. Разберём подробно.
{`// === ИГРА «ДВЕРЬ ПО КОДУ» — главный скрипт ===
// СЕКРЕТНЫЙ КОД — порядок кнопок. Поменяй на свой!
const CODE = [3, 1, 4, 2];
let entered = []; // что игрок уже нажал
let opened = false;
game.ui.showText('Нажми кнопки в правильном порядке (E)', 4);
// Кнопки и финиш сообщают сюда через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
// Номер нажатой кнопки приходит в data: { num }.
// игрок нажал кнопку с номером d.num
game.onMessage('press', (d) => {
if (opened) return;
game.sound.play('click');
entered.push(d.num);
// проверяем — совпадает ли начало с кодом
const i = entered.length - 1;
if (entered[i] !== CODE[i]) {
// ошибка — сброс
entered = [];
game.ui.showText('Неверно! Код сброшен.', 1.5);
game.sound.play('lose');
return;
}
// весь код введён верно
if (entered.length === CODE.length) {
opened = true;
game.ui.showText('Код верный! Дверь открывается.', 3);
game.sound.play('win');
const door = game.scene.findOne('Дверь');
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
} else {
game.ui.showText('Верно! Дальше...', 1);
}
});
// игрок встал на финиш
game.onMessage('win', () => {
game.ui.showText('Победа! Ты разгадал код!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Как работает проверка кода:
CODE = [3, 1, 4, 2] — секретный код,
массив номеров кнопок по порядку;entered — список того, что игрок уже нажал;game.onMessage('press', (d) => ...) —
ловит сообщение от кнопки. Номер кнопки приходит
в d.num. Кнопки шлют его через
game.broadcast('press', {'{ num: ... }'}) —
скрипты в разных «песочницах» общаются только
сообщениями;entered.push(d.num) — добавляем новое
нажатие в конец списка;entered[i] !== CODE[i] — проверяем: совпало
ли последнее нажатие с нужной цифрой кода. Не совпало —
entered = [] очищает список, начинай
заново;entered равна длине
CODE и всё совпало — код разгадан,
дверь уезжает твином.
На каждую кнопку — свой скрипт. Отличается только
цифра в press(...) и в подсказке.
{`// === Скрипт кнопки-цифры 1 ===
game.self.onInteract(() => {
// сообщаем главному скрипту номер нажатой кнопки
game.broadcast('press', { num: 1 }); // ← номер кнопки
}, {
text: 'Нажать кнопку 1',
distance: 3
});`}
Для «Кнопки_2» поставь {'{ num: 2 }'} и текст
«Нажать кнопку 2», и так далее.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}
В лавке за прилавком стоит NPC-торговец. Игрок подходит, нажимает E — торговец отвечает репликой и дарит ключ. С ключом можно открыть дверь и дойти до финиша.
game.scene.spawnNpc);npc.say;game.inventory).spawnNpc.
{`// === ИГРА «ТОРГОВЕЦ» — главный скрипт ===
game.ui.showText('Поговори с торговцем — нажми E у прилавка', 4);
// создаём NPC-торговца за прилавком
const trader = game.scene.spawnNpc('character-a', {
x: 0, y: 1, z: 5, // y=1 — на верху блоков пола (иначе утоплен)
name: 'Торговец Боб',
hp: 100,
speed: 0, // торговец стоит на месте
});
let hasKey = false;
// Прилавок, дверь и финиш сообщают сюда через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
// игрок заговорил с торговцем
game.onMessage('talk', () => {
if (hasKey) {
trader.say('Иди к двери, ключ у тебя!', 3);
return;
}
hasKey = true;
trader.say('Привет! Вот тебе ключ от двери. Удачи!', 4);
game.inventory.add({ name: 'Ключ' });
game.ui.showText('Ты получил Ключ!', 2);
game.sound.play('pickup');
});
// игрок пытается открыть дверь
game.onMessage('openDoor', () => {
if (!game.inventory.has('Ключ')) {
game.ui.showText('Дверь заперта. Нужен ключ от торговца.', 2);
return;
}
game.ui.showText('Ключ подошёл! Дверь открыта.', 3);
game.sound.play('win');
const door = game.scene.findOne('Дверь');
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
});
// игрок встал на финиш
game.onMessage('win', () => {
game.ui.showText('Победа! Ты прошёл лавку торговца!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём:
game.scene.spawnNpc('character-a', опции) —
создаёт NPC. speed: 0 — торговец не ходит,
стоит за прилавком;trader.say('текст', 4) — над головой NPC
на 4 секунды появляется реплика;game.inventory.add({'{ name: '}'Ключ'{' }'}) —
кладёт предмет в инвентарь игрока;game.inventory.has('Ключ') — проверяет,
есть ли предмет. Дверь открывается только с ключом.{`// === Скрипт прилавка ===
game.self.onInteract(() => {
game.broadcast('talk'); // сообщаем главному скрипту: говорим с торговцем
}, {
text: 'Поговорить с торговцем',
distance: 4
});`}
{`// === Скрипт двери ===
game.self.onInteract(() => {
game.broadcast('openDoor'); // сообщаем главному скрипту: открыть дверь
}, {
text: 'Открыть дверь',
distance: 4
});`}
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}
npc.moveTo.
На площадке вперемешку стоят жёлтые звёзды и синие кубы. Собирать нужно только звёзды — кубы это обманки. Игра помечает все звёзды специальной меткой-тегом, а потом по тегу легко считает, сколько звёзд ещё осталось.
game.scene.tag);game.scene.untag;game.scene.getTagged
возвращает список всех объектов с этим тегом; #ffd700, материал «Неон». Поставь 7 штук
и дай им имена «Звезда_1», «Звезда_2» ... «Звезда_7».
#3b82f6. Поставь 5 штук вперемешку
со звёздами. Имена для них не важны — собирать их не надо.
Главный скрипт сначала «наклеивает» тег на все звёзды, а потом считает, сколько звёзд осталось.
{`// === ИГРА «СОБЕРИ ПО ТЕГАМ» — главный скрипт ===
game.ui.showText('Собери все ЖЁЛТЫЕ звёзды!', 3);
game.ui.score = 0;
// помечаем все звёзды тегом 'звезда'.
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже,
// поэтому простановку тегов делаем через game.after.
game.after(0.2, () => {
for (let i = 1; i <= 7; i++) {
const star = game.scene.findOne('Звезда_' + i);
if (star) game.scene.tag(star, 'звезда');
}
});
// Звёзды сообщают о сборе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('collected', () => {
// getTagged вернёт все ещё не собранные звёзды
const left = game.scene.getTagged('звезда').length;
game.ui.score = 7 - left;
game.sound.play('coin');
if (left === 0) {
game.ui.showText('Победа! Все звёзды собраны!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});`}
Разберём построчно:
for внутри game.after(0.2, ...)
проходит по звёздам с 1 по 7, находит каждую по имени
через findOne (небольшая задержка нужна,
чтобы снимок сцены успел появиться);game.scene.tag(star, 'звезда') — навешивает
на звезду тег-ярлычок «звезда»;game.scene.getTagged('звезда') — возвращает
список всех объектов с тегом «звезда». А
.length — это сколько их в списке;7 - left — если осталось 5 звёзд, значит
собрано 2 (всего 7 минус 5). Это и показываем в счёте;if (left === 0) — звёзд с тегом не осталось,
все собраны — победа.Этот скрипт повесь на каждую из 7 звёзд.
{`// === Скрипт звезды ===
game.self.onTouch(() => {
// снимаем тег и удаляем звезду
game.scene.untag(game.self.ref, 'звезда');
game.self.delete();
game.broadcast('collected'); // сообщаем главному скрипту о сборе
});`}
Что происходит при касании:
game.scene.untag(game.self.ref, 'звезда') —
снимает тег «звезда» с этой звезды. Теперь
getTagged её больше не вернёт;game.self.delete() — звезда исчезает
со сцены;game.broadcast('collected') — шлёт
сообщение главному скрипту. Тот ловит его через
game.onMessage('collected', ...) и
пересчитывает остаток. Скрипты в разных «песочницах»
общаются только сообщениями.Настоящий тир: на постаментах стоят красные мишени-шары. Нужно кликать по ним мышкой — за каждое попадание мишень взрывается искрами и даёт очко. Выбей все 8 мишеней.
game.self.onClick);onTouch из прошлых уроков срабатывал, когда
игрок касается объекта телом. А onClick —
когда игрок щёлкает по объекту мышкой, даже издалека.
Для тира это как раз то, что нужно.
#ff3030, материал «Неон». Поставь по сфере
на каждый постамент, повыше. У мишеней «Столкновение»
выключи. Дай им имена «Мишень_2», «Мишень_4» и т.д.
{`// === ИГРА «ТИР» — главный скрипт ===
let score = 0;
const TOTAL = 8;
game.ui.score = 0;
game.ui.showText('Кликай по красным мишеням!', 3);
// Мишени сообщают о попадании через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('hit', () => {
score = score + 1;
game.ui.score = score;
game.sound.play('hit');
if (score >= TOTAL) {
game.ui.showText('Победа! Все мишени выбиты!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});`}
Здесь всё знакомо:
score — счётчик попаданий;TOTAL = 8 — всего мишеней;game.onMessage('hit', ...) — ловит
сообщение «hit» от мишеней. Каждая мишень шлёт его
через game.broadcast('hit'), когда по ней
попали. Внутри +1 к счёту и проверка: выбиты ли все 8.Главное в этом уроке — повесь скрипт на каждую мишень-сферу.
{`// === Скрипт мишени ===
// Клик по 3D-объекту = выстрел в него.
game.self.onClick(() => {
// взрыв искр на месте мишени
game.scene.spawnParticles('explosion', game.self.position,
{ count: 1, color: '#ff6633' });
game.self.delete(); // мишень сбита
game.broadcast('hit'); // сообщаем главному скрипту о попадании
});`}
Разберём:
game.self.onClick(() => {'{...}'}) —
срабатывает, когда игрок щёлкнул мышкой по этой мишени;game.scene.spawnParticles('explosion', ...) —
на месте мишени вспыхивает взрыв оранжевых искр;game.self.position — координаты самой
мишени, там и будет взрыв;game.self.delete() — мишень исчезает,
а game.broadcast('hit') шлёт сообщение
главному скрипту — тот добавляет очко. Скрипты в разных
«песочницах» общаются только сообщениями.Перед игроком — большое озеро раскалённой лавы. Идти по лаве нельзя — она жжёт и отнимает здоровье. Нужно перепрыгивать с одного каменного островка на другой, пока не доберёшься до зелёного финиша на той стороне.
game.player.damage);Чтобы лава наносила урон, над озером нужен один большой невидимый куб-зона. Он ловит касание игрока.
{`// === ИГРА «ЛАВА-ПОЛ» — главный скрипт ===
let won = false;
game.ui.showText('Прыгай по островкам! Лава жжёт!', 3);
// если HP кончилось — игрок воскреснет на старте сам.
// следим за падением в лаву ниже уровня.
game.onTick(() => {
if (won) return;
const p = game.player.position;
if (p && p.y < -2) {
game.player.respawn();
}
});
// Финиш сообщает о победе через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты перебрался через лаву!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
game.onTick следит, не провалился ли игрок
совсем низко — тогда возвращает его на старт;game.onMessage('win', ...) — победа.
Финиш шлёт сюда сообщение «win» через
game.broadcast('win').{`// === Скрипт лавы ===
// Игрок коснулся лавы — урон. У damage есть защита (i-frames),
// так что урон не каждый кадр, а раз в ~0.5 секунды.
game.self.onTouch(() => {
game.player.damage(20);
game.sound.play('hit');
});`}
Разберём:
game.player.damage(20) — отнимает у игрока
20 единиц здоровья. Когда HP дойдёт до нуля, игрок
сам воскреснет на старте;'hit' — сигнал «ой, жжётся!».{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win'); // сообщаем главному скрипту о победе
});`}
damage(35)) —
будет сложнее. Добавь по пути «аптечку» — зелёную сферу,
касание которой лечит через game.player.heal.
Сделай острова поменьше — прыгать точнее.
На поляне среди зелёных кустов спрятан золотой ключ. Игрок ищет ключ, подбирает его, а потом подходит к сундуку и открывает его клавишей E. Без ключа сундук не откроется.
game.inventory.add);game.inventory.has);game.self.onInteract). #2f7d32. Поставь 5-6 кустов вразброс
по поляне — среди них и спрячется ключ.
#ffd700, материал «Неон». Положи его
где-нибудь в уголке, рядом с кустом. «Столкновение»
у ключа выключи. Имя — «Ключ».
{`// === ИГРА «КЛЮЧ И СУНДУК» — главный скрипт ===
game.ui.showText('Найди ключ и открой сундук!', 3);
// Ключ и сундук сообщают сюда через broadcast/onMessage —
// скрипты в разных песочницах, связь только сообщениями.
// игрок подобрал ключ
game.onMessage('takeKey', () => {
game.inventory.add({ name: 'Ключ' });
game.ui.showText('Ты нашёл Ключ!', 2);
game.sound.play('pickup');
});
// игрок пытается открыть сундук
game.onMessage('openChest', () => {
if (!game.inventory.has('Ключ')) {
game.ui.showText('Сундук заперт. Сначала найди ключ.', 2);
return;
}
game.ui.showText('Победа! Сундук открыт — там сокровище!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Здесь два обработчика сообщений:
game.onMessage('takeKey', ...) — игрок
нашёл ключ. game.inventory.add({'{ name: '}'Ключ'{' }'})
кладёт предмет «Ключ» в рюкзак игрока;game.onMessage('openChest', ...) — попытка
открыть сундук. Сначала game.inventory.has('Ключ')
проверяет, есть ли ключ в рюкзаке. Если нет — сообщение
«Сундук заперт» и return прерывает функцию;game.broadcast(...) —
скрипты в разных «песочницах» общаются только
сообщениями.return — это «выйти из функции прямо сейчас».
Если ключа нет, дальше код просто не выполняется — сундук
остаётся закрытым.
{`// === Скрипт ключа ===
game.self.onTouch(() => {
game.broadcast('takeKey'); // сообщаем главному скрипту: ключ найден
game.self.delete(); // ключ подобран
});`}
Игрок коснулся ключа — скрипт шлёт сообщение
game.broadcast('takeKey') (главный скрипт
положит ключ в инвентарь), а game.self.delete()
убирает ключ с поляны: он же теперь в рюкзаке.
{`// === Скрипт сундука ===
game.self.onInteract(() => {
game.broadcast('openChest'); // сообщаем главному скрипту: открыть сундук
}, { text: 'Открыть сундук', distance: 4 });`}
Когда игрок подходит ближе чем на 4 метра, над сундуком появляется подсказка «Открыть сундук». Нажатие E шлёт сообщение «openChest» — а главный скрипт уже проверит, есть ли ключ.
Между двумя площадками висят большие качели. Они сами раскачиваются туда-сюда. Игрок запрыгивает на качели с возвышенности и, прокатившись на них, перебирается на финишную площадку.
game.constraints.hinge);hinge.setAngle;game.every
для раскачивания.Главный скрипт делает качели «настоящими»: вешает их на петлю и раскачивает.
{`// === ИГРА «КАЧЕЛИ» — главный скрипт ===
let won = false;
game.ui.showText('Запрыгни на качели и прокатись!', 3);
// делаем качели на петле и раскачиваем их.
// ВАЖНО: game.scene.findOne нельзя звать сразу в начале скрипта —
// снимок сцены приходит чуть позже. Ждём 0.2с через game.after.
let hinge = null;
let dir = -35;
game.after(0.2, () => {
const swing = game.scene.findOne('Качели');
hinge = game.constraints.hinge(swing, {
pivotX: 0, pivotZ: 0, // ось — посередине качелей
angle: 35
});
// раскачиваем туда-сюда каждые 1.4 секунды
game.every(1.4, () => {
if (won || !hinge) return;
hinge.setAngle(dir);
dir = -dir;
});
});
game.onTick(() => {
if (won) return;
const p = game.player.position;
if (p && p.y < -3) game.player.respawn();
});
// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты перебрался на качелях!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём по частям:
game.after(0.2, ...) — ждём 0.2 секунды
перед поиском качелей. Снимок сцены приходит чуть позже
старта, поэтому findOne сразу позвать
нельзя — качелей ещё «не видно»;game.scene.findOne('Качели') — находим
качели по имени;game.constraints.hinge(swing, опции) —
вешаем качели на петлю-шарнир. pivotX и
pivotZ — где ось вращения (0, 0 — точно
посередине качелей);hinge.setAngle(dir) — поворачивает качели
на заданный угол. -35 — наклон в одну сторону,
35 — в другую;dir = -dir — меняет знак: было -35, стало 35,
потом снова -35. Так качели качаются туда-сюда;game.every(1.4, ...) — каждые 1.4 секунды
меняем угол — вот и раскачивание.
Финиш ловит касание игрока и шлёт сообщение
'win' — главный скрипт его поймает и покажет
победу.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
В комнате два этажа — нижний и верхний. Между ними ездит синяя платформа-лифт: сама поднимается наверх и опускается вниз, без остановки. Игрок встаёт на лифт, дожидается верха и сходит на финиш.
repeat);yoyo: true означает:
доехал до конца — поезжай обратно. А repeat —
сколько раз так повторить.
#3357ff, материал «Неон». Поставь его
на нижнем этаже. «Столкновение» и «Закреплён» включены.
Имя — «Лифт».
{`// === ИГРА «ЛИФТ» — главный скрипт ===
let won = false;
game.ui.showText('Встань на синий лифт — он повезёт наверх', 3);
// лифт вечно ездит: вниз (y=1) ↔ верх (y=12.3).
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже.
game.after(0.2, () => {
const lift = game.scene.findOne('Лифт');
game.tween(lift, { y: 12.3 }, {
duration: 3.5,
yoyo: true, // обратно вниз
repeat: 999, // почти бесконечно
easing: 'ease'
});
});
game.onTick(() => {
if (won) return;
const p = game.player.position;
if (p && p.y < -3) game.player.respawn();
});
// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты поднялся на лифте!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём твин лифта:
game.after(0.2, ...) — ждём 0.2 секунды:
снимок сцены приходит чуть позже старта, поэтому
findOne сразу позвать нельзя;game.scene.findOne('Лифт') — находим лифт
по имени;game.tween(lift, {'{ y: 12.3 }'}, опции) —
плавно поднимает лифт на высоту 12.3;duration: 3.5 — подъём занимает 3.5 секунды;yoyo: true — доехав до верха, лифт сам
поедет обратно вниз;repeat: 999 — повторить этот путь 999 раз,
то есть практически бесконечно — лифт ездит всю игру;easing: 'ease' — лифт мягко разгоняется
и тормозит, а не дёргается.
Финиш ловит касание игрока и шлёт сообщение
'win'. Главный скрипт ловит его через
game.onMessage('win', ...) — так два скрипта
из разных «песочниц» общаются.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
duration: 5) или
быстрее. Добавь второй лифт, который ездит в другую
сторону. Поставь на верхнем этаже монетки из урока 1.
На арене стоят три врага: Гоблин, Скелет и Орк. Над головой каждого висит метка с его именем и здоровьем. Игрок подходит к врагу, кликает по нему — наносит урон, и метка показывает, сколько HP осталось. Победи всех троих.
game.scene.setLabel);game.scene.clearLabel;
Врагов ставить руками не нужно — они появятся из скрипта
командой spawnNpc, как торговец в уроке 13.
Вся игра в одном скрипте. Он создаёт врагов, вешает над ними метки и обрабатывает удары.
{`// === ИГРА «ИМЕНА НАД ВРАГАМИ» — главный скрипт ===
game.ui.showText('Победи всех врагов! Кликай по ним', 3);
// данные врагов: имя и позиция
const enemyData = [
{ name: 'Гоблин', x: -5, z: 3, hp: 60 },
{ name: 'Скелет', x: 4, z: 5, hp: 80 },
{ name: 'Орк', x: 0, z: 8, hp: 100 },
];
let alive = enemyData.length;
enemyData.forEach((d) => {
// создаём NPC-врага
const npc = game.scene.spawnNpc('character-b', {
x: d.x, y: 1, z: d.z, // y=1 — на верху блоков пола
name: d.name, hp: d.hp, speed: 0,
});
// вешаем над врагом метку-billboard с именем и HP
let hp = d.hp;
function updateLabel() {
game.scene.setLabel(npc.ref, d.name + ' HP: ' + hp, {
color: '#ff5555', height: 3
});
}
updateLabel();
// когда враг гибнет — убираем метку
npc.onDeath(() => {
game.scene.clearLabel(npc.ref);
alive = alive - 1;
game.sound.play('hit');
if (alive <= 0) {
game.ui.showText('Победа! Все враги повержены!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});
// урон врагу по клику игрока рядом — упрощённо бьём раз в клик
// (в этой игре кликаем по сцене возле врага)
game.onClick(() => {
if (hp <= 0) return;
// бьём только если игрок близко к этому врагу
const p = game.player.position;
if (!p || !npc.position) return;
const dist = Math.hypot(p.x - npc.position.x, p.z - npc.position.z);
if (dist < 4) {
hp = hp - 30;
if (hp < 0) hp = 0;
npc.damage(30);
updateLabel();
game.scene.spawnParticles('sparks', npc.position, { duration: 0.4 });
}
});
});`}
Разберём по частям. Сначала про создание врагов:
enemyData — список врагов: у каждого имя,
координаты и запас здоровья;enemyData.forEach((d) => {'{...}'}) —
перебираем список и для каждого делаем одно и то же;game.scene.spawnNpc('character-b', ...) —
создаёт NPC-врага. speed: 0 — враг стоит
на месте.Теперь про метку над врагом:
game.scene.setLabel(npc.ref, текст, опции) —
вешает над врагом надпись. Текст склеен из имени и HP:
получится «Гоблин HP: 60»;color — цвет надписи, height —
на какой высоте над врагом она висит;updateLabel() — функция, которая заново
пишет метку. Зовём её каждый раз, когда HP изменилось.Про удар и гибель:
game.onClick — игрок щёлкнул мышью.
Math.hypot(...) считает расстояние
до врага: бьём, только если ближе 4 метров;npc.damage(30) — наносим врагу 30 урона,
updateLabel() обновляет цифру HP в метке;npc.onDeath(...) — когда враг погиб,
game.scene.clearLabel(npc.ref) убирает
его метку, а счётчик alive уменьшается;alive дошёл до нуля — все враги
повержены, победа.enemyData —
просто допиши строчку. Сделай врагов посильнее (больше HP).
Поменяй цвет метки в зависимости от здоровья: зелёная,
пока HP высокое, красная — когда мало.
По полю за тобой гонится злой NPC-охотник. Он бегает чуть быстрее обычного и не отстаёт. Нужно петлять между каменными кубами-укрытиями и добежать до зелёного укрытия-финиша. Поймал — игра возвращает на старт.
game.scene.spawnNpc);enemy.follow('player'));Math.hypot).Главный скрипт создаёт врага, запускает погоню и следит, не догнал ли он игрока.
{`// === ИГРА «ПРЕСЛЕДОВАТЕЛЬ» — главный скрипт ===
let won = false;
game.ui.showText('Убегай от врага! Добеги до укрытия!', 3);
// создаём NPC-преследователя
const enemy = game.scene.spawnNpc('character-b', {
x: 0, y: 1, z: -3, // y=1 — на верху блоков пола (иначе утоплен)
name: 'Охотник', hp: 100, speed: 4,
});
// враг постоянно гонится за игроком
enemy.follow('player');
// каждый кадр проверяем — не догнал ли враг.
// enemy.position наполняется не сразу после spawnNpc (NPC появляется
// через кадр) — пока позиции нет, пропускаем кадр.
game.onTick(() => {
if (won) return;
const p = game.player.position;
const e = enemy.position;
if (!p || !e) return;
const dist = Math.hypot(p.x - e.x, p.z - e.z);
if (dist < 1.6) {
// враг поймал — на старт
game.player.respawn();
game.ui.showText('Пойман! Беги снова!', 2);
game.sound.play('lose');
}
});
// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
if (won) return;
won = true;
enemy.stop();
game.ui.showText('Победа! Ты убежал от врага!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём построчно:
game.scene.spawnNpc('character-b', опции) —
создаёт NPC прямо во время игры. В опциях — где
появится, имя, здоровье и speed —
скорость бега;enemy.follow('player') — командует NPC
«гонись за игроком». Дальше враг сам бежит за тобой
каждый кадр;Math.hypot(p.x - e.x, p.z - e.z) —
расстояние между игроком и врагом. Если оно меньше
1.6 — враг вплотную, значит поймал;enemy.stop() в победе — враг замирает,
погоня закончена.speed: 4 чуть больше скорости
обычной ходьбы. Если убегать слишком легко — увеличь её,
слишком сложно — уменьши.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
Когда игрок касается финиша, скрипт шлёт сообщение
game.broadcast('win'). Главный скрипт ловит
его через game.onMessage('win', ...) —
останавливает врага и показывает «Победа». Так два скрипта
из разных «песочниц» общаются сообщениями.
follow('player'). Сделай
укрытий побольше. Спрячь по полю монетки из урока 1 —
собирай их на бегу.
Длинный коридор, а посередине — большая красная зона. Пока игрок внутри неё, у него тает здоровье. Перед зоной лежит зелёная аптечка — подбери её, чтобы пополнить HP, и пробеги опасный участок до финиша.
onTouch и onUntouch;game.player.damage;game.player.heal;game.every.{`// === ИГРА «ЗОНА ОПАСНОСТИ» — главный скрипт ===
let inZone = false; // игрок сейчас в красной зоне?
let won = false;
game.ui.showText('Пробеги через красную зону к финишу!', 3);
// пока игрок в зоне — каждые 0.6с снимаем HP
game.every(0.6, () => {
if (won) return;
if (inZone) {
game.player.damage(12);
game.sound.play('hit');
}
});
// зона и финиш шлют сюда сообщения через broadcast — скрипты
// в разных песочницах, общие переменные между ними не видны.
game.onMessage('zone-enter', () => {
inZone = true;
game.ui.showText('Опасно! Беги быстрее!', 1.5);
});
game.onMessage('zone-leave', () => { inZone = false; });
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты прошёл зону опасности!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём:
inZone — флажок «игрок внутри красной
зоны». Скрипт зоны включает и выключает его
сообщениями;game.every(0.6, fn) — каждые 0.6 секунды
выполняет функцию. Внутри: если игрок в зоне —
game.player.damage(12) снимает 12 HP;game.onMessage('zone-enter', ...) и
game.onMessage('zone-leave', ...) — ловят
сообщения от зоны: «игрок вошёл» и «вышел». Зона шлёт
их через game.broadcast, потому что её
скрипт работает в отдельной «песочнице» и не видит
переменную inZone напрямую;game.onMessage('win', ...) — ловит
сообщение от финиша.{`// === Скрипт зоны опасности ===
// onTouch — игрок вошёл, onUntouch — вышел.
game.self.onTouch(() => {
game.broadcast('zone-enter');
});
game.self.onUntouch(() => {
game.broadcast('zone-leave');
});`}
Здесь важна пара событий: onTouch
срабатывает, когда игрок входит в зону, а
onUntouch — когда выходит. Так зона точно
знает, внутри игрок или нет.
{`// === Скрипт аптечки ===
game.self.onTouch(() => {
game.player.heal(60);
game.ui.showText('+60 HP', 1.5);
game.sound.play('pickup');
game.self.delete();
});`}
game.player.heal(60) — добавляет игроку
60 единиц здоровья. Аптечку взяли один раз —
game.self.delete() убирает её.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
Перед стеной с дверью стоят три красных рычага. Дёргать их (клавишей E) нужно в правильном порядке. Угадал последовательность — дверь уезжает вверх. Ошибся — все рычаги сбрасываются, начинай заново.
game.self.onInteract;Главный скрипт хранит правильный порядок и проверяет каждое нажатие.
{`// === ИГРА «ПЕРЕКЛЮЧАТЕЛИ» — главный скрипт ===
// правильный порядок рычагов
const ORDER = [2, 3, 1];
let pressed = [];
let opened = false;
game.ui.showText('Дёрни рычаги в нужном порядке (E)', 4);
// рычаги дёргаются через broadcast('lever', { num }) — скрипты
// в разных песочницах, состояние pressed[] живёт только здесь.
game.onMessage('lever', (d) => {
const n = d.num;
if (opened) return;
game.sound.play('click');
pressed.push(n);
const i = pressed.length - 1;
if (pressed[i] !== ORDER[i]) {
pressed = [];
game.ui.showText('Неверно! Рычаги сброшены.', 1.5);
game.sound.play('lose');
return;
}
if (pressed.length === ORDER.length) {
opened = true;
game.ui.showText('Верно! Дверь открыта.', 3);
game.sound.play('win');
const door = game.scene.findOne('Дверь');
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
} else {
game.ui.showText('Так держать!', 1);
}
});
game.onMessage('win', () => {
game.ui.showText('Победа! Ты разгадал порядок!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Самое интересное — проверка порядка:
ORDER = [2, 3, 1] — секретный порядок:
сначала второй рычаг, потом третий, потом первый.
Поменяй числа на свой порядок;pressed.push(n) — добавляет нажатый рычаг
в список того, что игрок уже дёрнул;if (pressed[i] !== ORDER[i]) — сравниваем
последнее нажатие с тем, что должно быть. Не совпало —
pressed = [] очищает список, начинай
заново;if (pressed.length === ORDER.length) —
все три рычага дёрнуты правильно — дверь уезжает
вверх через game.tween.На каждый рычаг свой скрипт — отличается только номер. Вот скрипт первого рычага:
{`// === Скрипт рычага 1 ===
game.self.onInteract(() => {
game.broadcast('lever', { num: 1 });
}, { text: 'Дёрнуть рычаг 1', distance: 3 });`}
Рычаг шлёт сообщение game.broadcast('lever', {'{ num: 1 }'}):
имя сообщения — 'lever', а второй кусок —
«посылка» с номером рычага. Главный скрипт ловит её
через game.onMessage('lever', ...) и узнаёт
номер из d.num.
На «Рычаг_2» — такой же скрипт, но с
num: 2 и текстом «Дёрнуть рычаг 2»,
на «Рычаг_3» — с num: 3. Номер сообщает
главному скрипту, какой именно рычаг дёрнули.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
ORDER.
Сделай рычаги одного цвета, чтобы порядок было сложнее
угадать. Покрась рычаг в зелёный, когда его дёрнули
правильно — через game.scene.setColor.
Над пропастью протянут мост из деревянных досок. Стоит игроку наступить на доску — через секунду она рушится. Значит, бежать нужно без остановки. Замешкался — проваливаешься в пропасть.
game.after — сделать
что-то через секунду, а не сразу;{`// === ИГРА «ПАДАЮЩИЙ МОСТ» — главный скрипт ===
let won = false;
game.ui.showText('Беги по мосту — доски рушатся!', 3);
game.onTick(() => {
if (won) return;
const p = game.player.position;
if (p && p.y < -3) {
game.player.respawn();
game.ui.showText('Упал в пропасть! Снова.', 1.5);
game.sound.play('lose');
}
});
// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Ты перебежал мост!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Главный скрипт game.onTick каждый кадр
следит за высотой игрока: упал ниже -3 — провалился
в пропасть, возвращаем на старт. А
game.onMessage('win', ...) ждёт сообщение
от финиша — финиш работает в своей «песочнице», поэтому
о победе он сообщает через game.broadcast.
Этот скрипт вешается на каждую доску моста.
{`// === Скрипт доски моста ===
let cracking = false;
game.self.onTouch(() => {
if (cracking) return;
cracking = true;
game.sound.play('click');
game.after(1, () => { game.self.delete(); });
});`}
Разберём:
cracking — флажок-защёлка. Игрок может
наступить на доску несколько раз, но запустить таймер
падения нужно только один раз;game.after(1, fn) — «через 1 секунду
выполни это». Внутри —
game.self.delete(), доска исчезает;'click' при касании — сигнал
игроку: «доска затрещала, беги!».{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
Когда игра начинается, камера сама красиво облетает уровень по нескольким точкам — показывает игроку, что его ждёт. Как только облёт закончился, управление возвращается, и игрок идёт к финишу.
game.camera.cutscene); game.onCutsceneDone;Главный скрипт запускает облёт камеры и ждёт, когда он закончится.
{`// === ИГРА «КАМЕРА-ОБЛЁТ» — главный скрипт ===
let won = false;
// при старте — облёт уровня камерой по точкам
game.camera.cutscene([
{ x: 0, y: 18, z: -10 },
{ x: 12, y: 12, z: 8 },
{ x: -12, y: 12, z: 18 },
{ x: 0, y: 10, z: 28 },
], { segDuration: 1.8 });
// когда облёт закончился — отдаём камеру игроку
game.onCutsceneDone(() => {
game.ui.showText('Вперёд, к зелёному финишу!', 3);
});
// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Уровень пройден!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём:
game.camera.cutscene([точки], опции) —
запускает кино-облёт. Камера летит по списку точек
одна за другой;{'{ x, y, z }'},
место, откуда камера смотрит на уровень;segDuration: 1.8 — сколько секунд камера
летит от одной точки до следующей;game.onCutsceneDone(fn) — функция внутри
выполнится, когда облёт закончится. Тут мы показываем
подсказку «Вперёд, к финишу!».
Финиш ловит касание игрока и шлёт сообщение
'win'. Главный скрипт ловит его через
game.onMessage('win', ...) — так скрипты
из разных «песочниц» общаются между собой.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
segDuration.
Поставь по пути монетки или препятствия, которые камера
покажет во время облёта.
По полю разбросаны золотые монетки. Стоит игроку подойти к монетке поближе — она сама подлетает к нему, будто притянутая магнитом. Собери все 8 монеток.
game.onTick +
Math.hypot);game.tween);{`// === ИГРА «МАГНИТ МОНЕТ» — главный скрипт ===
let score = 0;
const TOTAL = 8;
game.ui.score = 0;
game.ui.showText('Подходи к монеткам — они притянутся!', 3);
// монетки сообщают о сборе через broadcast('coin') — скрипты
// в разных песочницах, счётчик score живёт только здесь.
game.onMessage('coin', () => {
score = score + 1;
game.ui.score = score;
game.sound.play('coin');
if (score >= TOTAL) {
game.ui.showText('Победа! Все монетки собраны!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});`}
Главный скрипт простой — он только считает собранные
монетки и проверяет победу. Каждая монетка работает
в своей «песочнице», поэтому о сборе она сообщает через
game.broadcast('coin'), а главный скрипт
ловит сообщение через game.onMessage('coin', ...).
Вся «магнитная магия» — в скрипте самой монетки.
Этот скрипт вешается на каждую монетку.
{`// === Скрипт магнитной монетки ===
let flying = false; // монетка уже летит к игроку?
let taken = false;
game.onTick(() => {
if (taken) return;
const c = game.self.position;
const p = game.player.position;
// позиции могут быть не готовы первые кадры — ждём
if (!c || !p) return;
const dist = Math.hypot(p.x - c.x, p.z - c.z);
// игрок коснулся монетки — собрана
if (dist < 1.2) {
taken = true;
game.self.delete();
game.broadcast('coin'); // сообщить главному скрипту
return;
}
// подошёл близко — монетка летит к игроку
if (!flying && dist < 6) {
flying = true;
game.tween(game.self.ref,
{ x: p.x, y: p.y + 1, z: p.z },
{ duration: 0.5, easing: 'ease' });
}
});`}
Разберём по частям:
game.self.position и
game.player.position могут быть ещё
не готовы — проверка if (!c || !p) return
просто пропускает такой кадр;dist — расстояние
от монетки до игрока через Math.hypot;dist {'<'} 1.2 — игрок дотянулся
до монетки. Ставим taken, удаляем
монетку и засчитываем её;dist {'<'} 6 и монетка ещё
не летит — включаем flying и запускаем
game.tween к позиции игрока. Монетка
плавно подлетает за 0.5 секунды;flying и taken
нужны, чтобы твин запустился один раз и монетка
засчиталась один раз.dist {'<'} 6 запустит её снова.
6
на большее число, монетки будут лететь издалека. Сделай
редкую красную монетку, которая убегает от игрока,
а не притягивается.
Паркур, где платформы стоят так далеко друг от друга, что обычным прыжком до них не добраться. Скрипт включает игроку двойной прыжок — можно прыгнуть ещё раз прямо в воздухе и допрыгнуть до следующей платформы.
game.player.setDoubleJump);{`// === ИГРА «ДВОЙНОЙ ПРЫЖОК» — главный скрипт ===
let won = false;
// включаем игроку двойной прыжок — теперь можно прыгнуть
// ещё раз прямо в воздухе
game.player.setDoubleJump(true);
game.ui.showText('Жми Space ДВАЖДЫ — двойной прыжок!', 4);
game.onTick(() => {
if (won) return;
const p = game.player.position;
if (p && p.y < -3) {
game.player.respawn();
game.sound.play('lose');
}
});
// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Двойной прыжок освоен!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Главное здесь:
game.player.setDoubleJump(true) — даёт
игроку способность прыгать ещё раз в воздухе.
Достаточно одной этой строчки;game.onTick следит за падением — упал
ниже -3, возвращаем на старт.
Финиш ловит касание игрока и шлёт сообщение
'win'. Главный скрипт ловит его через
game.onMessage('win', ...) — так два скрипта
из разных «песочниц» общаются.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
game.tween
с yoyo: true. Поставь монетки между
платформами — собирай их в прыжке.
Коридор перегорожен четырьмя фиолетовыми стенами. Каждую можно сделать призрачной: кликнул по стене — она становится полупрозрачной и проходимой. Пройди сквозь все стены к финишу.
game.self.onClick;game.physics.passThrough);game.scene.setOpacity.{`// === ИГРА «ПРИЗРАЧНЫЕ СТЕНЫ» — главный скрипт ===
game.ui.showText('Кликай по фиолетовым стенам — пройди сквозь!', 4);
// финиш сообщает о победе через broadcast — скрипты в разных
// песочницах, общая переменная между ними не видна.
game.onMessage('win', () => {
game.ui.showText('Победа! Ты прошёл сквозь все стены!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Главный скрипт только обрабатывает победу — он ловит
сообщение 'win' от финиша через
game.onMessage. Вся «магия» — в скриптах
стен.
Этот скрипт вешается на каждую стену.
{`// === Скрипт призрачной стены ===
let ghost = false;
game.self.onClick(() => {
if (ghost) return;
ghost = true;
// стена становится проходимой и полупрозрачной
game.physics.passThrough(game.self.ref, true);
game.scene.setOpacity(game.self.ref, 0.25);
game.sound.play('click');
game.ui.showText('Стена стала призрачной!', 1.5);
});`}
Разберём:
game.self.onClick(fn) — функция внутри
срабатывает, когда игрок кликнул по этой стене мышкой;game.physics.passThrough(ref, true) —
убирает у стены столкновение: теперь сквозь неё можно
пройти;game.scene.setOpacity(ref, 0.25) —
делает стену почти прозрачной (0.25 — четверть
непрозрачности), чтобы было видно: она призрачная;ghost — флажок, чтобы клики после
первого ничего не делали.passThrough), потом
внешний вид (setOpacity) — игрок и видит,
и чувствует, что стена «расколдована».
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
game.after).
Игрок собирает монетки, разбросанные по комнате. Потом подходит к прилавку и покупает ключ за 5 монет. С ключом в инвентаре он открывает запертую дверь и доходит до финиша.
game.inventory.add) и проверять, есть ли
он (game.inventory.has);Главный скрипт — «мозг» магазина: считает монеты, продаёт ключ, открывает дверь.
{`// === ИГРА «МАГАЗИН» — главный скрипт ===
let coins = 0;
const PRICE = 5; // ключ стоит 5 монет
let bought = false;
game.ui.score = 0;
game.ui.showText('Собери монетки и купи ключ у продавца!', 4);
// монетки/прилавок/дверь/финиш шлют сюда сообщения через broadcast —
// скрипты в разных песочницах, состояние coins/bought живёт только здесь.
game.onMessage('coin', () => {
coins = coins + 1;
game.ui.score = coins;
game.sound.play('coin');
});
game.onMessage('buy', () => {
if (bought) {
game.ui.showText('Ключ уже куплен, иди к двери!', 2);
return;
}
if (coins < PRICE) {
game.ui.showText('Мало монет! Нужно ' + PRICE + ', есть ' + coins, 2);
game.sound.play('lose');
return;
}
bought = true;
coins = coins - PRICE;
game.ui.score = coins;
game.inventory.add({ name: 'Ключ' });
game.ui.showText('Куплен Ключ! Открой дверь.', 3);
game.sound.play('win');
});
game.onMessage('open-door', () => {
if (!game.inventory.has('Ключ')) {
game.ui.showText('Дверь заперта. Купи ключ в магазине.', 2);
return;
}
const door = game.scene.findOne('Дверь');
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
game.ui.showText('Дверь открыта!', 2);
});
game.onMessage('win', () => {
game.ui.showText('Победа! Ты прошёл магазин!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Каждый объект магазина — монетка, прилавок, дверь, финиш —
работает в своей «песочнице» и не видит переменные
главного скрипта. Поэтому объекты шлют сообщения через
game.broadcast('имя'), а главный скрипт ловит
их через game.onMessage('имя', ...). Разберём
четыре обработчика:
onMessage('coin', ...) — монетка собрана:
coins растёт, число на экране тоже;onMessage('buy', ...) — покупка ключа.
Сначала проверяем coins {'<'} PRICE —
хватает ли денег. Хватает — отнимаем 5 монет и
game.inventory.add кладёт ключ
в инвентарь;onMessage('open-door', ...) —
game.inventory.has('Ключ') проверяет,
есть ли ключ. Есть — дверь уезжает вверх;onMessage('win', ...) — победа на финише. add, проверил через has. Так
делают ключи, оружие, зелья — что угодно.
{`// === Скрипт монетки ===
game.self.onTouch(() => {
game.broadcast('coin');
game.self.delete();
});`}
{`// === Скрипт прилавка ===
game.self.onInteract(() => {
game.broadcast('buy');
}, { text: 'Купить ключ (5 монет)', distance: 4 });`}
{`// === Скрипт двери ===
game.self.onInteract(() => {
game.broadcast('open-door');
}, { text: 'Открыть дверь', distance: 4 });`}
И прилавок, и дверь — это объекты с взаимодействием по
E. Подошёл к прилавку и нажал
E — объект шлёт сообщение 'buy', подошёл
к двери и нажал E — летит сообщение 'open-door'.
Главный скрипт ловит эти сообщения и решает, что делать.
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('win');
});`}
На поле стоит NPC-старейшина. Он даёт цепочку из трёх заданий: собери монетку, дойди до синего флага, вернись к нему. Каждое задание открывает следующее. Выполнил всё — победа.
npc.say);Главный скрипт хранит этап квеста и ведёт игрока по заданиям.
{`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт ===
// этап квеста: 0=не начат, 1=собрать монетку, 2=дойти до флага,
// 3=вернуться к NPC, 4=готово
let stage = 0;
game.ui.showText('Подойди к квестодателю (E)', 3);
// создаём NPC рядом с тумбой
const npc = game.scene.spawnNpc('character-a', {
x: 1.5, y: 1, z: 2, name: 'Старейшина', hp: 100, speed: 0, // y=1 — на полу
});
// квестодатель/монетка/флаг шлют сюда сообщения через broadcast —
// скрипты в разных песочницах, этап квеста stage живёт только здесь.
game.onMessage('talk', () => {
if (stage === 0) {
stage = 1;
npc.say('Задание 1: найди жёлтую монетку!', 4);
game.ui.showText('Квест: собери монетку', 3);
} else if (stage === 3) {
stage = 4;
npc.say('Молодец! Квест выполнен!', 4);
game.ui.showText('Победа! Квест пройден!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
} else if (stage === 4) {
npc.say('Спасибо, герой!', 3);
} else {
npc.say('Сначала выполни задание!', 3);
}
});
game.onMessage('coin-done', () => {
if (stage !== 1) return;
stage = 2;
game.sound.play('coin');
npc.say('Отлично! Теперь дойди до синего флага.', 4);
game.ui.showText('Квест: дойди до флага', 3);
});
game.onMessage('flag-done', () => {
if (stage !== 2) return;
stage = 3;
game.sound.play('pickup');
game.ui.showText('Квест: вернись к квестодателю', 3);
});`}
Главное здесь — переменная stage:
stage хранит, на каком шаге квеста
игрок: 0 — не начат, 1 — собирает монетку, 2 — идёт
к флагу, 3 — возвращается, 4 — готово; stage, поэтому шлют сообщения через
game.broadcast('имя'), а главный скрипт
ловит их через game.onMessage('имя', ...);onMessage('talk', ...) — разговор с NPC.
Что он скажет, зависит от stage: на старте
даёт первое задание, в конце поздравляет;onMessage('coin-done', ...) и
onMessage('flag-done', ...) — ловят
сообщения от монетки и флага. Они проверяют
stage и переводят квест на следующий
этап;npc.say('текст', 4) — NPC показывает
над собой облачко с репликой на 4 секунды.if (stage !== 1) return важны:
они не дают засчитать задание раньше времени. Монетку
нельзя «сдать», пока NPC не дал задание её собрать.
{`// === Скрипт квестодателя ===
game.self.onInteract(() => {
game.broadcast('talk');
}, { text: 'Поговорить', distance: 4 });`}
{`// === Скрипт квест-монетки ===
game.self.onTouch(() => {
game.broadcast('coin-done');
game.self.delete();
});`}
{`// === Скрипт квест-флага ===
game.self.onTouch(() => {
game.broadcast('flag-done');
});`}
game.inventory.add. Поставь
второго NPC со своим квестом.
Из дальнего конца поля к твоей базе идут волны NPC-врагов. Кликай по ним, чтобы уничтожать. Уничтожь 12 врагов — победа. Пропустишь 5 врагов до базы — поражение.
game.every + spawnNpc);enemy.moveTo;enemy.remove);Враги создаются скриптом — заранее их ставить не нужно.
Это большой скрипт — разберём его по частям.
{`// === ИГРА «ЗАЩИТА БАЗЫ» — главный скрипт ===
let killed = 0; // сколько врагов уничтожено
let leaked = 0; // сколько врагов дошло до базы
const GOAL = 12; // победа — уничтожить 12 врагов
const MAX_LEAK = 5; // проигрыш — 5 врагов прорвались
let over = false;
game.ui.score = 0;
game.ui.showText('Защити базу! Кликай по врагам', 3);
// каждые 2 секунды появляется новый враг далеко от базы
let total = 0;
game.every(2, () => {
if (over || total >= GOAL + MAX_LEAK) return;
total = total + 1;
const x = game.random(-8, 8);
const enemy = game.scene.spawnNpc('character-b', {
x: x, y: 1, z: 38, name: 'Враг', hp: 30, speed: 2.5, // y=1 — на полу
});
// враг идёт к базе (точка 0,0,0)
enemy.moveTo(0, 2);
let dead = false;
// клик по врагу — уничтожить
game.onClick(() => {
if (dead || over) return;
const p = game.player.position;
const e = enemy.position;
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 5) {
dead = true;
enemy.remove();
game.scene.spawnParticles('explosion', e, { count: 1 });
game.sound.play('hit');
killed = killed + 1;
game.ui.score = killed;
if (killed >= GOAL && !over) {
over = true;
game.ui.showText('Победа! База защищена!', 5);
game.sound.play('win');
}
}
});
// если враг дошёл до базы — пропуск
const watch = game.every(0.4, () => {
if (dead || over) { game.cancel(watch); return; }
// enemy.position готов не сразу после spawnNpc — ждём
const ep = enemy.position;
if (ep && ep.z < 4) {
dead = true;
game.cancel(watch);
enemy.remove();
leaked = leaked + 1;
game.sound.play('lose');
game.ui.showText('Враг прорвался! (' + leaked + '/' + MAX_LEAK + ')', 2);
if (leaked >= MAX_LEAK && !over) {
over = true;
game.ui.showText('База разрушена! Поражение.', 5);
}
}
});
});`}
Как появляются волны врагов:
game.every(2, ...) — каждые 2 секунды
создаётся новый враг в случайной точке у дальнего
края (z: 38);enemy.moveTo(0, 2) — командует врагу
идти к точке базы (координата 0) со скоростью 2.Что происходит с каждым врагом:
game.onClick(...) — клик мышкой. Если
игрок близко к врагу (меньше 5 метров) —
enemy.remove() убирает врага, летит
взрыв, killed растёт;game.every(0.4, ...) в переменной
watch — следит за врагом. Если он дошёл
до базы (z {'<'} 4) — счётчик
leaked растёт;game.cancel(watch) — останавливает этот
таймер-следилку, когда враг уже мёртв или ушёл;killed {'>'}= GOAL —
победа, leaked {'>'}= MAX_LEAK —
поражение.game.onClick и свой
таймер watch. Флажок dead —
личный для каждого врага: он не даёт убить или засчитать
одного врага дважды.
Кольцевая трасса с четырьмя чекпоинтами по углам. Нужно проехать 2 круга, пересекая чекпоинты строго по порядку. Сверху идёт секундомер — старайся уложиться побыстрее.
game.ui.timer +
dt.{`// === ИГРА «ГОНКА С КРУГАМИ» — главный скрипт ===
const LAPS = 2; // сколько кругов проехать
const CP_COUNT = 4; // чекпоинтов на круге
let nextCp = 0; // какой чекпоинт ждём (0..3)
let lap = 0; // текущий круг
let time = 0;
let won = false;
game.ui.timer = 0;
game.ui.showText('Проедь 2 круга через чекпоинты!', 3);
game.onTick((dt) => {
if (won) return;
time = time + dt;
game.ui.timer = time;
});
// чекпоинты шлют сюда broadcast('checkpoint', { num }) — скрипты
// в разных песочницах, прогресс гонки nextCp/lap живёт только здесь.
game.onMessage('checkpoint', (d) => {
// игрок пересёк чекпоинт с номером n (1..4)
const n = d.num;
if (won) return;
// ждём именно следующий по порядку чекпоинт
if (n - 1 !== nextCp) return;
game.sound.play('click');
nextCp = nextCp + 1;
if (nextCp >= CP_COUNT) {
nextCp = 0;
lap = lap + 1;
if (lap >= LAPS) {
won = true;
const t = Math.round(time * 10) / 10;
game.ui.showText('Финиш! Круги пройдены за ' + t + ' сек', 6);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
} else {
game.ui.showText('Круг ' + lap + ' из ' + LAPS + '!', 2);
}
}
});`}
Главное здесь — порядок чекпоинтов:
game.onMessage('checkpoint', (d) => {'{...}'}) —
ловит сообщение от чекпоинта. Каждый чекпоинт — отдельный
скрипт в своей «песочнице», поэтому номер приходит
в «посылке» d.num, а не через общую
переменную;nextCp — номер чекпоинта, который мы
ждём следующим (начинаем с 0);if (n - 1 !== nextCp) return — если
игрок пересёк не тот чекпоинт (например,
проехал второй раньше первого) — пропускаем, он
не засчитан;nextCp сдвигается
дальше. Прошёл все 4 — lap (круг)
растёт, а nextCp сбрасывается в 0;if (lap {'>'}= LAPS) — проехал 2 круга,
секундомер останавливается, показывается время.На каждый чекпоинт — свой скрипт, отличается только номер. Вот скрипт первого чекпоинта:
{`// === Скрипт чекпоинта 1 ===
game.self.onTouch(() => {
game.broadcast('checkpoint', { num: 1 });
});`}
Чекпоинт шлёт сообщение
game.broadcast('checkpoint', {'{ num: 1 }'}):
имя сообщения — 'checkpoint', а «посылка»
с номером говорит главному скрипту, какую точку пересёк
игрок. На «Чекпоинт_2» — такой же скрипт с
num: 2, на «Чекпоинт_3» — с num: 3,
на «Чекпоинт_4» — с num: 4.
LAPS на 3.
Поставь на трассе препятствия. Покрась пройденный
чекпоинт в зелёный через game.scene.setColor,
чтобы было видно, где ты уже был.
Сначала паркур — допрыгай по платформам до высокой арены. Там тебя ждёт NPC-босс с большим запасом здоровья. Кликай по нему, пока полоска HP не обнулится. Над боссом висит метка с его здоровьем.
game.scene.setLabel);boss.onDeath.Босса заранее ставить не нужно — его создаёт скрипт, когда игрок добирается до арены.
{`// === ИГРА «ПЛАТФОРМЕР С БОССОМ» — главный скрипт ===
let won = false;
let bossSpawned = false;
let bossHp = 120;
game.ui.showText('Пройди паркур до арены босса!', 3);
// упал — на старт
game.onTick(() => {
if (won) return;
const p = game.player.position;
if (!p) return; // позиция игрока может быть не готова первые кадры
if (p.y < -3) {
game.player.respawn();
game.sound.play('lose');
}
// дошёл до арены — спавним босса один раз
if (!bossSpawned && p.z > 24 && p.y > 5) {
bossSpawned = true;
const boss = game.scene.spawnNpc('character-b', {
x: 0, y: 7, z: 32, name: 'БОСС', hp: bossHp, speed: 2, // y=7 — на полу арены (блоки y=6)
});
boss.follow('player');
game.scene.setLabel(boss.ref, 'БОСС HP: ' + bossHp, {
color: '#ff3333', height: 3.5
});
game.ui.showText('БОСС! Кликай по нему!', 3);
boss.onDeath(() => {
won = true;
game.scene.clearLabel(boss.ref);
game.ui.showText('Победа! Босс повержен!', 5);
game.sound.play('win');
const p2 = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p2.x, y: p2.y + 3, z: p2.z }, { duration: 3, count: 3 });
});
// удар по боссу кликом, если игрок близко
game.onClick(() => {
if (won) return;
const pp = game.player.position;
const bp = boss.position;
if (pp && bp && Math.hypot(pp.x - bp.x, pp.z - bp.z) < 5) {
bossHp = bossHp - 20;
if (bossHp < 0) bossHp = 0;
boss.damage(20);
game.scene.setLabel(boss.ref, 'БОСС HP: ' + bossHp, {
color: '#ff3333', height: 3.5
});
game.scene.spawnParticles('sparks', bp, { duration: 0.4 });
game.sound.play('hit');
}
});
}
});`}
Разберём по частям. Сначала — паркур:
game.onTick следит за падением: упал
ниже -3 — респаун;if (!bossSpawned && p.z {'>'} 24 && p.y {'>'} 5) —
как только игрок зашёл на арену (далеко по
z и высоко по y) —
создаём босса. Флажок bossSpawned
не даёт создать его второй раз.Потом — бой:
boss.follow('player') — босс гонится
за игроком;game.scene.setLabel(boss.ref, текст, опции) —
вешает над боссом метку-табличку с его HP. Это
billboard — она всегда повёрнута к камере;game.onClick — клик по боссу, если игрок
близко: bossHp уменьшается на 20,
boss.damage(20) ранит NPC, метка
обновляется;boss.onDeath(fn) — когда у босса HP
дошло до нуля, эта функция выполнится: победа,
метку убираем через clearLabel.hp: 120, удар снимает 20 — значит
нужно 6 точных кликов. Меняй эти числа, чтобы сделать
бой легче или сложнее.
На грядках растут шесть растений. Каждое медленно растёт (анимация-твин) и за 5 секунд становится спелым — жёлтым. Только спелое растение можно собрать клавишей E. Собрал слишком рано — не считается. Собери весь урожай.
game.tween по размеру);onDone — сделать что-то,
когда твин закончился;{`// === ИГРА «СБОР УРОЖАЯ» — главный скрипт ===
let harvested = 0;
const GOAL = 6;
game.ui.score = 0;
game.ui.showText('Дождись, пока растения вырастут, и собери!', 4);
// растения сообщают о сборе через broadcast('harvested') — скрипты
// в разных песочницах, счётчик harvested живёт только здесь.
game.onMessage('harvested', () => {
harvested = harvested + 1;
game.ui.score = harvested;
game.sound.play('coin');
if (harvested >= GOAL) {
game.ui.showText('Победа! Весь урожай собран!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});`}
Главный скрипт считает собранные растения и проверяет
победу. Каждое растение работает в своей «песочнице»,
поэтому о сборе оно сообщает через
game.broadcast('harvested'), а главный
скрипт ловит сообщение через
game.onMessage('harvested', ...). Вся
«грядочная магия» — в скрипте растения.
Этот скрипт вешается на каждое растение.
{`// === Скрипт растения ===
let ripe = false; // растение выросло (спелое)?
let picked = false;
// растение медленно растёт за 5 секунд. Вместе с sy растёт и y —
// чтобы низ конуса оставался на земле (растение «растёт из земли»).
game.tween(game.self.ref,
{ sx: 1.3, sy: 2.6, sz: 1.3, y: 2.3 },
{ duration: 5, onDone: () => {
ripe = true;
game.scene.setColor(game.self.ref, '#ffcc33'); // спелое — жёлтое
}
});
// собрать растение (E)
game.self.onInteract(() => {
if (picked) return;
if (!ripe) {
game.ui.showText('Ещё не выросло! Подожди.', 1.5);
return;
}
picked = true;
game.self.delete();
game.broadcast('harvested');
}, { text: 'Собрать', distance: 3 });`}
Разберём:
game.tween(ref, {'{ sx, sy, sz, y }'}, опции) —
твин по размеру. Растение плавно увеличивается
с маленького до большого за 5 секунд. Заодно растёт
и y — чтобы низ конуса оставался на земле,
а не уходил под неё;onDone: () => {'{...}'} — функция,
которая выполнится, когда твин закончится.
Внутри: ставим ripe = true (растение
спелое) и красим его в жёлтый;game.self.onInteract — сбор по
E. Если
!ripe — растение ещё не поспело,
показываем подсказку и выходим;picked = true, растение
исчезает и засчитывается.ripe меняет правила: до того как он
стал true, собрать растение нельзя. Это
и есть «состояние» объекта.
По полю ходит NPC-искатель и постоянно идёт к игроку. Среди поля расставлены стены-укрытия. Нужно петлять за ними и не дать себя поймать целых 40 секунд. Поймал — игрок возвращается на старт, но таймер не сбрасывается.
spawnNpc + follow('player');game.ui.timer +
dt);Math.hypot
между игроком и NPC.NPC-искатель создаётся скриптом — ставить его заранее не нужно.
Вся игра — в одном скрипте. Разберём его.
{`// === ИГРА «ПРЯТКИ ОТ NPC» — главный скрипт ===
let time = 0;
const SURVIVE = 40; // продержись 40 секунд
let won = false;
game.ui.timer = 0;
game.ui.showText('Прячься за стенами 40 секунд!', 4);
// NPC-искатель ходит за игроком
const seeker = game.scene.spawnNpc('character-b', {
x: 0, y: 1, z: 10, name: 'Искатель', hp: 100, speed: 3, // y=1 — на полу
});
seeker.follow('player');
game.onTick((dt) => {
if (won) return;
time = time + dt;
game.ui.timer = time;
// искатель поймал — на старт, время продолжается.
// seeker.position появляется через кадр после spawnNpc — ждём.
const p = game.player.position;
const e = seeker.position;
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 1.7) {
game.player.respawn();
game.ui.showText('Найден! Прячься снова!', 1.5);
game.sound.play('lose');
}
// продержался — победа
if (time >= SURVIVE) {
won = true;
seeker.stop();
game.ui.showText('Победа! Ты прятался 40 секунд!', 5);
game.sound.play('win');
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});`}
Разберём:
game.scene.spawnNpc(...) создаёт
искателя, seeker.follow('player') —
он гонится за игроком;time = time + dt каждый кадр копит
прошедшее время, game.ui.timer выводит
секундомер;Math.hypot(...) {'<'} 1.7 — искатель
вплотную. Игрок возвращается на старт, но
time не обнуляется — таймер идёт
дальше;if (time {'>'}= SURVIVE) — продержался
40 секунд: seeker.stop() останавливает
искателя, показывается «Победа».На поле стоят 3 коричневых ящика и 3 зелёные плиты-цели. Игрок подходит к ящику и нажимает E — ящик плавно перепрыгивает на следующую клетку. Задача — расставить все ящики по их плитам. Получилось — победа.
game.tween);#22dd55, материал
«Неон». Это плиты-цели. «Столкновение» — выключи, чтобы
ящик мог встать прямо на плиту.
#b5651d. Каждый ящик — напротив
своей плиты, но в дальнем ряду. Дай имена «Ящик_1»,
«Ящик_2», «Ящик_3».
Главный скрипт хранит, какие ящики уже на плитах, и ловит момент победы.
{`// === ИГРА «ГОЛОВОЛОМКА С ЯЩИКАМИ» — главный скрипт ===
// для каждого ящика — на какой плите он сейчас (true/false)
const onPlate = [false, false, false];
let won = false;
game.ui.showText('Поставь все 3 ящика на зелёные плиты!', 4);
// Ящики сообщают сюда через game.broadcast('box', { i, on }).
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('box', (d) => {
// ящик с номером d.i (0..2) встал/сошёл с плиты
onPlate[d.i] = d.on;
if (d.on) game.sound.play('click');
// все три ящика на плитах?
if (!won && onPlate[0] && onPlate[1] && onPlate[2]) {
won = true;
game.ui.showText('Победа! Все ящики на местах!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
});`}
Разберём:
onPlate — массив из трёх «галочек»: стоит
ли ящик №0, №1, №2 на плите;game.onMessage('box', ...) — главный скрипт
ждёт сообщение «box» от ящиков. В d.i —
номер ящика, в d.on — стоит ли он на плите;if (onPlate[0] {'&&'} onPlate[1] {'&&'} onPlate[2])
— победа, только когда все три «галочки» стоят разом.
Скрипт висит на ящике. Каждый ящик ходит по своему ряду
из 5 клеток. Тут показан ящик №1 (для второго и третьего
поменяй число i в сообщении на 1 и 2).
{`// === Скрипт ящика 1 ===
// ряд позиций по Z, по которым прыгает ящик
const ROW = [-6, -3, 0, 3, 6];
let cell = 0; // на какой клетке ряда стоит ящик
const PLATE_Z = 6; // плита в конце ряда
game.self.onInteract(() => {
// перепрыгиваем на следующую клетку (по кругу)
cell = (cell + 1) % ROW.length;
const z = ROW[cell];
game.tween(game.self.ref, { z: z }, { duration: 0.4, easing: 'ease' });
// сообщаем главному скрипту — стоит ли ящик на плите
game.broadcast('box', { i: 0, on: z === PLATE_Z });
}, { text: 'Двинуть ящик', distance: 3 });`}
Что происходит:
ROW — пять клеток ряда (значения Z);cell = (cell + 1) % ROW.length — переходим
к следующей клетке; % заворачивает с конца
ряда обратно к началу;game.tween(...) — плавно сдвигает ящик
на новую клетку за 0.4 секунды;z === PLATE_Z — проверка «ящик на плите?»:
результат (true/false) уходит
в поле on сообщения;game.broadcast('box', ...) — шлёт сообщение
«box» главному скрипту, а тот ловит его через
game.onMessage('box', ...).i: первый шлёт
{'{'} i: 0 {'}'}, второй — {'{'} i: 1 {'}'},
третий — {'{'} i: 2 {'}'}. Не перепутай числа!
Длинная трасса с препятствиями: красные шипы наносят урон, в полу — ямы, над одной ямой ездит синяя платформа. Посередине стоит жёлтый чекпоинт — пройдёшь его, и после падения игра вернёт тебя сюда, а не на старт. В конце — зелёный финиш.
game.player.damage);yoyo: true;game.player.setSpawn); onTick.{`// === ИГРА «ПОЛОСА ПРЕПЯТСТВИЙ» — главный скрипт ===
let won = false;
game.ui.showText('Пройди полосу: шипы, ямы, платформа!', 4);
// движущаяся платформа ездит над ямой туда-сюда.
// findOne нельзя сразу в начале — снимок сцены приходит чуть позже.
game.after(0.2, () => {
const mover = game.scene.findOne('ДвижПлатформа');
game.tween(mover, { x: 3 }, {
duration: 2, yoyo: true, repeat: 999, easing: 'ease'
});
});
game.onTick(() => {
if (won) return;
const p = game.player.position;
if (p && p.y < -3) {
game.player.respawn(); // упал в яму — на чекпоинт/старт
game.sound.play('lose');
}
});
// Чекпоинт и финиш сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('checkpoint', () => {
// ставим точку возрождения на чекпоинт
game.player.setSpawn({ x: -0.5, y: 1, z: 24 });
game.ui.showText('Чекпоинт сохранён!', 2);
game.sound.play('pickup');
});
game.onMessage('finish', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Полоса пройдена!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём:
game.after(0.2, ...) — ждём чуть-чуть,
потому что findOne в самом начале ещё не
видит объекты сцены;game.tween(mover, ...) с yoyo: true
и repeat: 999 — платформа бесконечно
ездит туда и обратно;onTick ловит падение в яму
(p.y {'<'} -3) и возвращает игрока;game.onMessage('checkpoint', ...) ловит
сообщение от чекпоинта и зовёт
game.player.setSpawn(...) — теперь
респаун будет в середине трассы;game.onMessage('finish', ...) — победа
с конфетти, флажок won защищает от повтора.{`// === Скрипт шипа ===
game.self.onTouch(() => {
game.player.damage(25);
game.sound.play('hit');
});`}
{`// === Скрипт чекпоинта ===
game.self.onTouch(() => {
game.broadcast('checkpoint');
});`}
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish');
});`}
Шип при касании отнимает 25 здоровья. Чекпоинт сохраняет точку возрождения. Финиш зовёт победу.
setSpawn меняет место, куда вернёт
respawn. До чекпоинта это старт, после —
середина трассы. Так игроку не приходится бежать заново.
На поле 4 цветные плитки-ноты. Игра сначала проигрывает мелодию из 5 нот — слушай внимательно. Потом нужно повторить её, нажимая плитки E в том же порядке. Ошибся — мелодия начинается сначала.
game.after; game.sound.Главный скрипт хранит мелодию, проигрывает её и проверяет, верно ли игрок повторяет.
{`// === ИГРА «МУЗЫКАЛЬНАЯ ИГРА» — главный скрипт ===
const SOUNDS = ['coin', 'jump', 'click', 'hit']; // плитки 1..4
// загаданная последовательность из 5 нот
const SEQ = [1, 3, 2, 4, 1];
let playerStep = 0; // на каком шаге игрок
let won = false;
let canPress = false;
game.ui.showText('Слушай мелодию, потом повтори!', 3);
// проигрываем мелодию: нота за нотой каждые 0.8 сек
SEQ.forEach((note, i) => {
game.after(1 + i * 0.8, () => {
game.sound.play(SOUNDS[note - 1]);
game.ui.showText('Нота ' + (i + 1) + ' из ' + SEQ.length, 0.7);
});
});
// после мелодии разрешаем игроку повторять
game.after(1 + SEQ.length * 0.8 + 0.5, () => {
canPress = true;
game.ui.showText('Теперь повтори мелодию!', 3);
});
// Плитки сообщают сюда нажатую ноту через game.broadcast('press', { n }).
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('press', (d) => {
if (won || !canPress) return;
// правильная нота?
if (d.n === SEQ[playerStep]) {
playerStep = playerStep + 1;
if (playerStep >= SEQ.length) {
won = true;
game.ui.showText('Победа! Мелодия повторена верно!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
} else {
// ошибка — сброс
playerStep = 0;
game.ui.showText('Ошибка! Слушай и пробуй снова.', 2);
game.sound.play('lose');
}
});`}
Разберём:
SEQ = [1, 3, 2, 4, 1] — загаданная мелодия:
номера нот по порядку;SEQ.forEach((note, i) ={'>'} ...) — для
каждой ноты ставим таймер: нота i прозвучит
через 1 + i * 0.8 секунд;canPress — флажок: нажимать можно только
после того, как мелодия доиграла;game.onMessage('press', ...) ловит сообщение
от плитки; d.n — номер нажатой ноты;if (d.n === SEQ[playerStep]) — нажатая нота
совпала с нужной? Да — шаг вперёд; нет — сброс
playerStep в ноль.
Скрипт висит на каждой плитке. Тут показана нота №1 —
для остальных поменяй звук и число в press.
{`// === Скрипт ноты-плитки 1 ===
game.self.onInteract(() => {
game.sound.play('coin');
game.scene.spawnParticles('sparks', game.self.position,
{ duration: 0.4, color: '#e23b3b' });
game.broadcast('press', { n: 1 });
}, { text: 'Сыграть ноту', distance: 3 });`}
При нажатии E плитка играет
свой звук, вспыхивает искрами и шлёт
game.broadcast('press', {'{'} n: 1 {'}'}) —
сообщение со своим номером. У ноты №2 будет
{'{'} n: 2 {'}'}, у №3 —
{'{'} n: 3 {'}'} и т.д.
spawnParticles и звук должны
совпадать с цветом плитки — так игроку понятнее, что
он нажал.
SEQ до 7-8 нот. Сделай 5-ю
плитку. Добавь подсветку: пусть плитка на миг меняет цвет,
когда игра её проигрывает.
Над землёй висят 8 полупрозрачных «призрачных» блоков один над другим. Игрок подходит к призраку и нажимает E — блок становится настоящим: плотным и коричневым. Строить нужно строго снизу вверх. Построил всю башню — победа.
passThrough,
setOpacity, setCollide);#88aaff, материал
«Стекло» — куб станет полупрозрачным.
«Столкновение» выключи: сквозь призрак можно пройти.
{`// === ИГРА «БАШНЯ — СТРОЙКА» — главный скрипт ===
const STEPS = 8;
let placed = 0; // сколько блоков поставлено
game.ui.score = 0;
game.ui.showText('Подходи к местам и ставь блоки (E) снизу вверх', 4);
// Места-призраки сообщают сюда через game.broadcast('place', { n }).
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('place', (d) => {
// блок поставлен в место номер d.n (1..STEPS);
// ставить можно только следующее по порядку место
if (d.n !== placed + 1) {
game.ui.showText('Сначала поставь блок ниже!', 1.5);
return;
}
placed = placed + 1;
game.ui.score = placed;
game.sound.play('click');
if (placed >= STEPS) {
game.ui.showText('Победа! Башня построена!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
} else {
game.ui.showText('Блок ' + placed + ' из ' + STEPS, 1.5);
}
});`}
Разберём:
placed — сколько блоков уже стоит;game.onMessage('place', ...) ловит сообщение
от места-призрака; d.n — его номер;if (d.n !== placed + 1) — если игрок жмёт
не на следующий по очереди блок, стройка не засчитывается;game.ui.score = placed — счётчик показывает
прогресс башни;placed дошёл до 8 — победа.
Скрипт висит на каждом призраке. Тут показано место №1 —
у остальных поменяй число в place.
{`// === Скрипт места под блок 1 ===
let built = false;
game.self.onInteract(() => {
if (built) return;
// ставим настоящий блок — делаем призрак твёрдым и непрозрачным
game.physics.passThrough(game.self.ref, false);
game.scene.setOpacity(game.self.ref, 1);
game.scene.setColor(game.self.ref, '#b5651d');
game.scene.setCollide(game.self.ref, true);
built = true;
game.broadcast('place', { n: 1 });
}, { text: 'Поставить блок', distance: 4 });`}
Что происходит при нажатии:
passThrough(ref, false) — сквозь блок
больше нельзя пройти;setOpacity(ref, 1) — блок становится
полностью непрозрачным;setColor(ref, '#b5651d') — перекрашиваем
в коричневый «деревянный»;setCollide(ref, true) — теперь на блок
можно встать.n в сообщении:
место_1 шлёт {'{'} n: 1 {'}'}, место_2 —
{'{'} n: 2 {'}'} и так до 8.
Открытая арена. На игрока накатывают 3 волны врагов-NPC, одна за другой. С каждой волной врагов всё больше. Кликай по врагам, чтобы их уничтожать. Отбил все 3 волны — победа.
spawnNpc) и заставлять их преследовать; game.onClick. spawnNpc — заранее ставить их не нужно.
Вся игра — в одном глобальном скрипте: он запускает волны и считает врагов.
{`// === ИГРА «ВЫЖИВАНИЕ ОТ ВОЛН» — главный скрипт ===
const WAVES = 3; // всего волн
let wave = 0;
let won = false;
game.ui.showText('Отбей 3 волны врагов! Кликай по ним', 3);
// запускаем очередную волну
function startWave() {
if (won) return;
wave = wave + 1;
game.ui.showText('Волна ' + wave + ' из ' + WAVES + '!', 3);
game.sound.play('hit');
const count = wave + 2; // врагов всё больше: 3, 4, 5
let aliveInWave = count;
for (let i = 0; i < count; i++) {
// враг появляется по краю поля
const angle = (i / count) * 6.28;
const ex = Math.cos(angle) * 10;
const ez = Math.sin(angle) * 10;
const enemy = game.scene.spawnNpc('character-b', {
x: ex, y: 1, z: ez, name: 'Враг', hp: 40, speed: 2, // y=1 — на полу
});
enemy.follow('player');
let dead = false;
game.onClick(() => {
if (dead || won) return;
const p = game.player.position;
const e = enemy.position;
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 5) {
dead = true;
enemy.remove();
game.scene.spawnParticles('explosion', e, { count: 1 });
game.sound.play('hit');
aliveInWave = aliveInWave - 1;
// вся волна перебита — следующая
if (aliveInWave <= 0) {
if (wave >= WAVES) {
won = true;
game.ui.showText('Победа! Все волны отбиты!', 5);
game.sound.play('win');
const pp = game.player.position;
if (pp) game.scene.spawnParticles('confetti',
{ x: pp.x, y: pp.y + 3, z: pp.z }, { duration: 3, count: 3 });
} else {
game.after(2, startWave);
}
}
}
});
}
}
game.after(2, startWave); // первая волна через 2 секунды`}
Разберём:
startWave() — функция одной волны. Она
создаёт wave + 2 врагов: 3, потом 4, потом 5;Math.cos / Math.sin — расставляют врагов
по кругу вокруг арены;enemy.follow('player') — враг идёт
к игроку;game.onClick(...) — при клике проверяем
Math.hypot(...) {'<'} 5: враг рядом — он
уничтожен;if (aliveInWave {'<='} 0) ... game.after(2, startWave)
— вся волна перебита, через 2 секунды функция
запускает сама себя для следующей волны. Это
и есть рекурсия.startWave запускает startWave
снова, пока не кончатся волны. Главное — есть условие
остановки (wave {'>='} WAVES), иначе волны
шли бы бесконечно.
hp) или скорости (speed) —
станет сложнее. Добавь между волнами «передышку» подлиннее.
Большой уровень-приключение: длинная цепочка платформ-паркура, на пути — золотые монетки, посередине жёлтый чекпоинт, а на самом верху — финиш-сокровище. Эта игра собирает вместе почти всё, что ты прошёл в простых уроках.
{`// === ИГРА «ПЛАТФОРМЕР-ПРИКЛЮЧЕНИЕ» — главный скрипт ===
let coins = 0;
let won = false;
game.ui.score = 0;
game.ui.showText('Доберись до сокровища! Собирай монетки', 4);
game.onTick(() => {
if (won) return;
// позиция игрока может быть ещё не готова первые кадры —
// проверяем p перед обращением к p.y.
const p = game.player.position;
if (p && p.y < -3) {
game.player.respawn();
game.sound.play('lose');
}
});
// Монетки, чекпоинт и сокровище сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('coin', () => {
coins = coins + 1;
game.ui.score = coins;
game.sound.play('coin');
});
game.onMessage('checkpoint', () => {
game.player.setSpawn({ x: -0.5, y: 7, z: 28 });
game.ui.showText('Чекпоинт! Дальше — отсюда.', 2);
game.sound.play('pickup');
});
game.onMessage('treasure', () => {
if (won) return;
won = true;
game.ui.showText('Победа! Сокровище и ' + coins + ' монет!', 6);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём:
game.onMessage('coin', ...) — пришла
монетка, +1 к счёту;game.onMessage('checkpoint', ...) —
перенести точку возрождения наверх;game.onMessage('treasure', ...) — победа:
в надписи показываем, сколько монеток успел собрать;onTick — следит за падением, как
в уроках 2 и 37.game.broadcast.
{`// === Скрипт монетки ===
game.self.onTouch(() => {
game.broadcast('coin');
game.self.delete();
});`}
{`// === Скрипт чекпоинта ===
game.self.onTouch(() => {
game.broadcast('checkpoint');
});`}
{`// === Скрипт сокровища ===
game.self.onTouch(() => {
game.broadcast('treasure');
});`}
Монетка при касании засчитывается и исчезает. Чекпоинт сохраняет место. Сокровище зовёт победу.
Маленькая деревня с двумя жителями: старостой и кузнецом. Поговори со старостой — он даст квест: найти потерянный амулет за домом. Подними амулет и отнеси его кузнецу — получишь награду. Это настоящая RPG с этапами квеста.
spawnNpc, say);stage,
которая помнит, на каком шаге игрок;game.inventory);{`// === ИГРА «RPG-ДЕРЕВНЯ» — главный скрипт ===
// этап: 0=начало, 1=ищем амулет, 2=несём кузнецу, 3=готово
let stage = 0;
game.ui.showText('Деревня. Поговори со старостой (E)', 4);
const elder = game.scene.spawnNpc('character-a', {
x: 1.6, y: 1, z: 2, name: 'Староста', hp: 100, speed: 0, // y=1 — на полу
});
const smith = game.scene.spawnNpc('character-b', {
x: 12.6, y: 1, z: 7, name: 'Кузнец', hp: 100, speed: 0, // y=1 — на полу
});
// Староста, амулет и кузнец сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('elderTalk', () => {
if (stage === 0) {
stage = 1;
elder.say('Найди потерянный амулет за домом!', 4);
game.ui.showText('Квест: найди фиолетовый амулет', 3);
} else if (stage === 1) {
elder.say('Амулет всё ещё не у тебя...', 3);
} else {
elder.say('Спасибо за помощь деревне!', 3);
}
});
game.onMessage('takeAmulet', () => {
if (stage !== 1) return;
stage = 2;
game.inventory.add({ name: 'Амулет' });
game.sound.play('pickup');
game.ui.showText('Амулет найден! Отнеси кузнецу.', 3);
});
game.onMessage('smithTalk', () => {
if (stage === 2 && game.inventory.has('Амулет')) {
stage = 3;
game.inventory.remove('Амулет');
smith.say('Отличный амулет! Вот награда, герой!', 4);
game.ui.showText('Победа! Квест RPG-деревни выполнен!', 6);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
} else if (stage === 3) {
smith.say('Доброго пути!', 3);
} else {
smith.say('Принеси мне амулет — поговори со старостой.', 4);
}
});`}
Разберём:
stage — переменная-этап: 0 начало,
1 ищем амулет, 2 несём кузнецу, 3 квест выполнен;spawnNpc(...) со speed: 0 —
житель стоит на месте;elder.say(...) — над NPC появляется
реплика;game.inventory.add / has / remove —
кладём амулет в сумку, проверяем и забираем;game.onMessage('elderTalk', ...) и
game.onMessage('smithTalk', ...) говорят
разное в зависимости от stage — это
и есть «живой» квест.{`// === Скрипт старосты ===
game.self.onInteract(() => {
game.broadcast('elderTalk');
}, { text: 'Поговорить со старостой', distance: 4 });`}
{`// === Скрипт кузнеца ===
game.self.onInteract(() => {
game.broadcast('smithTalk');
}, { text: 'Поговорить с кузнецом', distance: 4 });`}
{`// === Скрипт амулета ===
game.self.onTouch(() => {
game.broadcast('takeAmulet');
game.self.delete();
});`}
Гоночная трасса на время. По дороге — синие плитки-бусты: наступил, и игрок ускоряется. И красные шипы-ловушки: задел — урон и замедление. Доберись до финиша как можно быстрее, секундомер покажет твоё время.
game.player.setSpeed); onTick и game.ui.timer;{`// === ИГРА «ГОНКА С ПРЕПЯТСТВИЯМИ» — главный скрипт ===
let time = 0;
let won = false;
game.ui.timer = 0;
game.ui.showText('Гонка! Синее ускоряет, шипы мешают', 4);
game.onTick((dt) => {
if (won) return;
time = time + dt;
game.ui.timer = time;
});
// Бусты, шипы и финиш сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('boost', () => {
// ускоряем игрока на 3 секунды
game.player.setSpeed(1.8);
game.sound.play('pickup');
game.ui.showText('УСКОРЕНИЕ!', 1);
game.after(3, () => game.player.setSpeed(1));
});
game.onMessage('spike', () => {
game.player.damage(15);
game.player.setSpeed(0.5); // шип замедляет
game.sound.play('hit');
game.after(1.5, () => game.player.setSpeed(1));
});
game.onMessage('finish', () => {
if (won) return;
won = true;
const t = Math.round(time * 10) / 10;
game.ui.showText('Финиш! Время: ' + t + ' сек', 6);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём:
onTick((dt) ={'>'} ...) — dt
это секунды с прошлого кадра; складываем их в
time — получается секундомер;game.onMessage('boost', ...) —
setSpeed(1.8) делает игрока в 1.8 раза
быстрее, а game.after(3, ...) через
3 секунды возвращает обычную скорость
setSpeed(1);game.onMessage('spike', ...) — отнимает
здоровье и наоборот замедляет до
setSpeed(0.5) на 1.5 секунды;Math.round(time * 10) / 10 — округляем
время до десятых долей секунды.{`// === Скрипт буста ===
game.self.onTouch(() => {
game.broadcast('boost');
});`}
{`// === Скрипт шипа-ловушки ===
game.self.onTouch(() => {
game.broadcast('spike');
});`}
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish');
});`}
setSpeed — множитель скорости. 1 — обычная,
1.8 — быстро, 0.5 — медленно. После эффекта всегда
возвращай setSpeed(1), иначе игрок навсегда
останется быстрым или медленным.
Враги идут по каменной дороге к твоей синей базе. По бокам дороги — площадки: подойди и нажми E, чтобы построить башню. Башни сами стреляют по врагам рядом. Уничтожь 14 врагов, не пропустив 8 — победа.
moveTo);game.scene.spawn);game.every;Главный скрипт делает всё: спавнит врагов, заставляет башни стрелять и проверяет, кто победил.
{`// === ИГРА «TOWER DEFENSE» — главный скрипт ===
let leaked = 0; // врагов прошло до базы
const MAX_LEAK = 8;
let killed = 0;
const GOAL = 14; // победа — уничтожить 14 врагов
let over = false;
// список башен: {x, z}
const towers = [];
// список живых врагов: {npc, alive}
const enemies = [];
game.ui.score = 0;
game.ui.showText('Ставь башни (E)! Не пропусти врагов', 4);
// Площадки сообщают сюда о постройке башни через
// game.broadcast('addTower', { x, z }).
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('addTower', (d) => {
towers.push({ x: d.x, z: d.z });
game.sound.play('click');
game.ui.showText('Башня построена!', 1.5);
});
// спавн врагов
let total = 0;
game.every(2.2, () => {
if (over || total >= GOAL + MAX_LEAK) return;
total = total + 1;
const npc = game.scene.spawnNpc('character-b', {
x: -0.5, y: 1, z: -3, name: 'Враг', hp: 50, speed: 2,
});
npc.moveTo(-0.5, 42);
const rec = { npc: npc, alive: true };
enemies.push(rec);
npc.onDeath(() => {
rec.alive = false;
killed = killed + 1;
game.ui.score = killed;
if (killed >= GOAL && !over) {
over = true;
game.ui.showText('Победа! База защищена!', 5);
game.sound.play('win');
}
});
});
// башни стреляют: каждые 0.8с бьём врага рядом с любой башней
game.every(0.8, () => {
if (over) return;
for (const t of towers) {
for (const e of enemies) {
if (!e.alive) continue;
const p = e.npc.position;
// позиция NPC появляется через кадр после spawn — пропускаем
if (!p) continue;
if (Math.hypot(p.x - t.x, p.z - t.z) < 7) {
e.npc.damage(25);
game.scene.spawnParticles('sparks', p, { duration: 0.3 });
break; // одна башня — один выстрел за тик
}
}
}
});
// проверка прорыва к базе
game.every(0.5, () => {
if (over) return;
for (const e of enemies) {
if (e.alive && e.npc.position && e.npc.position.z > 40) {
e.alive = false;
e.npc.remove();
leaked = leaked + 1;
game.sound.play('lose');
game.ui.showText('Враг прорвался! (' + leaked + '/' + MAX_LEAK + ')', 2);
if (leaked >= MAX_LEAK && !over) {
over = true;
game.ui.showText('База разрушена! Поражение.', 5);
}
}
}
});`}
Разберём:
towers и enemies — два списка:
где стоят башни и какие враги живы;game.onMessage('addTower', ...) ловит
сообщение от площадки и кладёт координаты башни
в список towers;game.every(2.2, ...) — каждые
2.2 секунды выпускает врага и зовёт
npc.moveTo(...) — враг идёт к базе;game.every(0.8, ...) — каждая башня
ищет врага ближе 7 единиц и бьёт его;game.every(0.5, ...) — проверяет,
не дошёл ли враг до базы (position.z {'>'} 40);killed {'>='} GOAL, поражение —
leaked {'>='} MAX_LEAK.{`// === Скрипт площадки под башню ===
let built = false;
game.self.onInteract(() => {
if (built) return;
built = true;
const pos = game.self.position;
// ставим башню — жёлтый цилиндр поверх площадки
game.scene.spawn('primitive:cylinder', {
x: pos.x, y: pos.y + 2.5, z: pos.z,
sx: 1.5, sy: 3, sz: 1.5,
color: '#ffcc33',
});
game.broadcast('addTower', { x: pos.x, z: pos.z });
}, { text: 'Построить башню', distance: 4 });`}
При нажатии E скрипт создаёт
жёлтый цилиндр-башню над площадкой и шлёт сообщение
game.broadcast('addTower', ...) с координатами
башни — главный скрипт ловит его, кладёт башню в список,
и она начинает стрелять.
game.every. Так делают почти все простые
Tower Defense: башня просто бьёт ближайшего врага по таймеру.
Жёлтый цилиндр-башня — это просто заглушка. Можно поставить вместо неё свою воксельную модель, которую ты сделал в редакторе моделей проекта.
3). game.scene.spawn('primitive:cylinder', ...)
на такой:{`// ставим башню — своя воксельная модель (id = 3)
game.scene.spawn('user:3', {
x: pos.x, y: pos.y + 2, z: pos.z,
rotationY: 0,
});`}
У пользовательских моделей нет sx/sy/sz и
color — размер и цвет задаются в самом
редакторе модели. Из параметров только позиция и
rotationY (поворот по вертикальной оси).
Высоту y подбери под высоту своей модели,
чтобы она ровно встала на площадку.
Закрытая арена. Со всех сторон к игроку сбегаются враги-NPC. Кликай по врагам — они гибнут. Но и враги бьют тебя, если подошли вплотную: у игрока есть здоровье. Перебей 15 врагов, не потеряв всё HP.
game.every + damage); onHpChange;game.cancel,
когда враг умер.{`// === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт ===
let score = 0;
const GOAL = 15;
let over = false;
game.ui.score = 0;
game.ui.showText('Перебей 15 врагов! Кликай по ним', 3);
// проигрыш, когда HP игрока кончилось
game.onHpChange((e) => {
if (!over && e.hp <= 0) {
over = true;
game.ui.showText('Поражение! Тебя одолели враги.', 5);
}
});
// спавним нового врага каждые 1.8 сек
game.every(1.8, () => {
if (over || score >= GOAL) return;
const angle = game.random(0, 6.28);
const ex = Math.cos(angle) * 11;
const ez = Math.sin(angle) * 11;
const enemy = game.scene.spawnNpc('character-b', {
x: ex, y: 1, z: ez, name: 'Враг', hp: 30, speed: 2.2, // y=1 — на полу
});
enemy.follow('player');
let dead = false;
// враг бьёт игрока, если подошёл вплотную
const dmgTimer = game.every(0.7, () => {
if (dead || over) { game.cancel(dmgTimer); return; }
const p = game.player.position;
const e = enemy.position;
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 1.8) {
game.player.damage(10);
game.sound.play('hit');
}
});
// клик по врагу — убить
game.onClick(() => {
if (dead || over) return;
const p = game.player.position;
const e = enemy.position;
if (p && e && Math.hypot(p.x - e.x, p.z - e.z) < 6) {
dead = true;
game.cancel(dmgTimer);
enemy.remove();
game.scene.spawnParticles('explosion', e, { count: 1 });
game.sound.play('hit');
score = score + 1;
game.ui.score = score;
if (score >= GOAL && !over) {
over = true;
game.ui.showText('Победа! Арена зачищена!', 5);
game.sound.play('win');
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
}
});
});`}
Разберём:
game.onHpChange((e) ={'>'} ...) —
срабатывает, когда здоровье игрока меняется. Если
e.hp {'<='} 0 — поражение;game.every(1.8, ...) — каждые 1.8 секунды
появляется новый враг по краю арены и
follow('player') — идёт к игроку;dmgTimer — отдельный таймер: пока враг жив,
каждые 0.7 секунды проверяет, рядом ли он, и бьёт;game.cancel(dmgTimer) — когда враг умер,
его таймер урона выключается, чтобы не работал зря; dead, свой таймер dmgTimer,
свой обработчик клика. Когда враг гибнет, его механизм
аккуратно выключается через game.cancel.
hp и
speed). Увеличь цель до 25 врагов.
В центре площадки — большой жёлтый куб. Кликай по нему, и копятся очки. По бокам — две кнопки-улучшения: красная добавляет силу клика, синяя включает авто-доход (очки капают сами). Накопи 200 очков — победа.
game.self.onClick; game.every; checkWin.{`// === ИГРА «КЛИКЕР» — главный скрипт ===
let points = 0; // очки
let perClick = 1; // очков за клик
let autoIncome = 0; // очков в секунду автоматически
const GOAL = 200;
let won = false;
game.ui.score = 0;
game.ui.showText('Кликай по жёлтому кубу! Цель: 200 очков', 4);
// авто-доход: каждую секунду прибавляем autoIncome
game.every(1, () => {
if (won) return;
if (autoIncome > 0) {
points = points + autoIncome;
game.ui.score = points;
checkWin();
}
});
function checkWin() {
if (!won && points >= GOAL) {
won = true;
game.ui.showText('Победа! Накоплено 200 очков!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
}
}
// Куб-кликер и кнопки апгрейда сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('click', () => {
if (won) return;
points = points + perClick;
game.ui.score = points;
game.sound.play('click');
checkWin();
});
game.onMessage('buyPower', () => {
if (points < 20) {
game.ui.showText('Нужно 20 очков для улучшения!', 1.5);
return;
}
points = points - 20;
perClick = perClick + 2;
game.ui.score = points;
game.sound.play('pickup');
game.ui.showText('Сила клика: +' + perClick + ' за клик', 2);
});
game.onMessage('buyAuto', () => {
if (points < 40) {
game.ui.showText('Нужно 40 очков для авто-дохода!', 1.5);
return;
}
points = points - 40;
autoIncome = autoIncome + 3;
game.ui.score = points;
game.sound.play('pickup');
game.ui.showText('Авто-доход: +' + autoIncome + ' в секунду', 2);
});`}
Разберём:
points — очки, perClick —
сколько даёт один клик, autoIncome —
сколько капает само в секунду;game.every(1, ...) — каждую секунду
прибавляет autoIncome к очкам;checkWin() — общая функция: проверка победы
вызывается и из клика, и из авто-дохода;game.onMessage('click', ...) ловит клик
по кубу — +perClick очков;game.onMessage('buyPower', ...) — за 20 очков
добавляет +2 к силе клика;game.onMessage('buyAuto', ...) — за 40 очков
добавляет +3 к авто-доходу.{`// === Скрипт куба-кликера ===
game.self.onClick(() => {
game.broadcast('click');
// куб слегка вспыхивает
game.scene.spawnParticles('sparks', game.self.position, { duration: 0.3 });
});`}
{`// === Скрипт улучшения «сила клика» (20 очков) ===
game.self.onInteract(() => {
game.broadcast('buyPower');
}, { text: 'Купить +силу клика (20)', distance: 3 });`}
{`// === Скрипт улучшения «авто-доход» (40 очков) ===
game.self.onInteract(() => {
game.broadcast('buyAuto');
}, { text: 'Купить авто-доход (40)', distance: 3 });`}
perClick
и autoIncome отдельной надписью через
game.ui.set.
Игрок заперт в комнате. Чтобы выбраться, нужно найти и нажать 3 красные кнопки. Две на виду, а третья спрятана за ящиком — её надо обойти. Нажал все три — дверь поднимается, и можно выйти к финишу.
{`// === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт ===
let pressed = 0; // сколько кнопок нажато
const TOTAL = 3;
let escaped = false;
game.ui.showText('Найди и нажми 3 кнопки, чтобы выйти!', 4);
// Кнопки и финиш сообщают сюда через game.broadcast.
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('pressButton', () => {
pressed = pressed + 1;
game.sound.play('click');
game.ui.showText('Кнопка ' + pressed + ' из ' + TOTAL, 1.5);
if (pressed >= TOTAL) {
// все кнопки — открываем дверь
const door = game.scene.findOne('Дверь');
game.tween(door, { y: 9 }, { duration: 1.2, easing: 'ease' });
game.ui.showText('Все кнопки нажаты! Дверь открыта!', 3);
game.sound.play('win');
}
});
game.onMessage('escape', () => {
if (escaped) return;
escaped = true;
game.ui.showText('Победа! Ты сбежал из комнаты!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
});`}
Разберём:
pressed — счётчик нажатых кнопок,
TOTAL — сколько их всего;game.onMessage('pressButton', ...) ловит
сообщение, которое кнопка шлёт при нажатии;if (pressed {'>='} TOTAL) — когда нажаты
все три, находим дверь по имени и поднимаем её твином
вверх;game.onMessage('escape', ...) — победа,
когда игрок прошёл через открытую дверь к финишу.{`// === Скрипт кнопки 1 ===
let used = false;
game.self.onInteract(() => {
if (used) return;
used = true;
game.scene.setColor(game.self.ref, '#22dd55'); // нажата — зелёная
game.broadcast('pressButton');
}, { text: 'Нажать кнопку', distance: 3 });`}
{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('escape');
});`}
Кнопка при нажатии становится зелёной (видно, что нажата),
шлёт game.broadcast('pressButton') и больше
не срабатывает благодаря флажку used.
'pressButton'. Главный скрипт
сам считает, сколько кнопок нажато.
Догонялки для нескольких игроков. Один игрок — водящий, он догоняет остальных, а они убегают и прячутся за укрытиями. Это мультиплеерная игра: чтобы играть с друзьями, её нужно опубликовать с галочкой «Мультиплеер».
game.players:
список всех, кто играет;game.room: данные,
которые видят все игроки; onPlayerJoin / onPlayerLeave; game.room.onChange.{`// === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт ===
//
// Это МУЛЬТИПЛЕЕРНАЯ игра. Чтобы играть с друзьями, опубликуй её
// с галочкой «Мультиплеер» — тогда в комнату смогут зайти несколько
// игроков. В одиночку игра показывает только правила.
game.ui.showText('Салки! Опубликуй игру для игры с друзьями', 4);
// общий счётчик игроков комнаты — виден всем
function refresh() {
const n = game.players.count();
game.ui.set('info', 'Игроков в комнате: ' + n, { x: 50, y: 8 });
// водящий выбирается так: первый зашедший игрок
const all = game.players.all();
if (all.length > 0) {
game.room.set('tagger', all[0].sessionId);
}
}
// при входе/выходе игрока — обновляем
game.onPlayerJoin((p) => {
game.ui.showText(p.name + ' присоединился к салкам!', 2);
refresh();
});
game.onPlayerLeave(() => refresh());
refresh();
// если ты водящий — догоняй других; если убегаешь — прячься.
game.room.onChange('tagger', (taggerId) => {
const me = game.players.me();
if (me && me.sessionId === taggerId) {
game.ui.showText('Ты ВОДЯЩИЙ! Догоняй и осаль других!', 3);
} else {
game.ui.showText('Убегай от водящего!', 3);
}
});`}
Разберём:
game.players.count() — сколько игроков
сейчас в комнате;game.players.all() — список всех игроков;
me() — это «я»;game.room.set('tagger', ...) — записываем
в общее состояние комнаты, кто водящий. Это видят
все игроки;game.onPlayerJoin / onPlayerLeave —
события входа и выхода игрока;game.room.onChange('tagger', ...) —
срабатывает у всех, когда водящий поменялся: каждый
узнаёт, он водящий или убегает.let score) есть только у тебя, а
game.room — общие для всей комнаты. Поэтому
«кто водящий» хранят именно в game.room.
game.ui.set. Сделай так, чтобы при касании
водящего с убегающим они менялись ролями. Добавь таймер
раунда.
Гонка нескольких игроков по одной трассе. Кто первым добежит до финиша — его имя записывается в общий счёт комнаты, и все игроки видят победителя. Это снова мультиплеерная игра.
game.room
хранит имя победителя для всех;game.players.me()
и имя игрока;game.room.onChange.{`// === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт ===
//
// Мультиплеерная гонка. Чтобы соревноваться с друзьями — опубликуй
// игру с галочкой «Мультиплеер».
game.ui.showText('Гонка! Беги к финишу первым', 3);
// показываем, сколько игроков и кто уже финишировал
function refresh() {
const n = game.players.count();
const winner = game.room.get('winner');
let txt = 'Игроков: ' + n;
if (winner) txt = txt + ' | Победил: ' + winner;
game.ui.set('info', txt, { x: 50, y: 8 });
}
refresh();
game.onPlayerJoin(() => refresh());
game.onPlayerLeave(() => refresh());
// когда кто-то финишировал — обновляем у всех
game.room.onChange('winner', () => refresh());
// Финиш сообщает сюда через game.broadcast('finish').
// Скрипты живут в РАЗНЫХ песочницах — общая переменная между ними не
// видна, поэтому связь только через сообщения broadcast/onMessage.
game.onMessage('finish', () => {
// если победитель ещё не определён — записываем себя
if (!game.room.get('winner')) {
const me = game.players.me();
const myName = me ? me.name : 'Игрок';
game.room.set('winner', myName);
game.ui.showText('Ты пришёл первым! Победа!', 5);
game.sound.play('win');
const p = game.player.position;
game.scene.spawnParticles('confetti',
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
} else {
game.ui.showText('Финиш! Но кто-то был быстрее.', 4);
}
});`}
Разберём:
game.room.get('winner') — читаем общую
переменную комнаты: записан ли уже победитель;refresh() — рисует надпись со счётом:
число игроков и имя победителя;game.onMessage('finish', ...) — если
победителя ещё нет, игрок записывает своё имя
через game.room.set('winner', myName);if (!game.room.get('winner')) — защита:
только первый добежавший станет победителем,
остальные увидят «кто-то был быстрее»;game.room.onChange('winner', ...) — как
только победитель записан, надпись обновляется
у всех игроков сразу.{`// === Скрипт финиша ===
game.self.onTouch(() => {
game.broadcast('finish');
});`}
Когда любой игрок касается финиша, скрипт шлёт сообщение
game.broadcast('finish') — а главный скрипт
ловит его и уже решает, первый этот игрок или нет.
game.room — это «общая доска»
комнаты. Один игрок пишет на неё имя победителя, и все
остальные тут же это видят.
Это последний урок — и он особенный. Здесь нет готовой игры. Тебе даётся пустая песочница: ровная зелёная площадка и один скрипт-приветствие. Твоя задача — придумать и собрать свою собственную игру с нуля. Всё, что нужно, ты уже знаешь из уроков 1-49.
Сначала реши, какая у тебя игра. Жанр — это «вид» игры. Вот какие ты уже умеешь делать:
Прежде чем строить — представь свою игру. Ответь себе на вопросы:
Можно даже нарисовать план на бумаге — это помогает не запутаться.
У каждой игры должна быть понятная цель — то, ради чего игрок играет. Без цели игра скучная. Цель может быть такой:
И обязательно покажи игроку, когда он победил —
надписью game.ui.showText('Победа!', 5),
звуком game.sound.play('win') и конфетти.
Сцена сама по себе не «живая» — её оживляют скрипты.
Начинай с главного скрипта: в нём заводи переменные
(счёт, флажок победы) и лови сообщения через
game.onMessage('имя', fn). На объекты вешай
небольшие скрипты — они шлют сообщения главному через
game.broadcast('имя'). Так главный скрипт
узнаёт, что монетку собрали или кнопку нажали. Ты делал
так в каждом уроке.
game.broadcast('имя'),
другой ловит game.onMessage('имя', fn). Можно
передать данные: game.broadcast('имя', {'{'} ... {'}'}).
Базовый набор инструментов, который ты знаешь:
game.self.onTouch — реакция на касание;game.self.onInteract — реакция на
E;game.self.onClick — реакция на клик;game.broadcast и game.onMessage
— связь между скриптами;game.onTick — каждый кадр;game.after и game.every —
таймеры;game.tween — плавное движение;game.scene.spawnNpc — враги и NPC;game.ui.score и
game.ui.showText — счёт и подсказки.Не строй сразу всё. Делай по чуть-чуть и почаще нажимай Запустить: построил пол — проверь, добавил врага — проверь. Так легче найти ошибку. Если что-то не работает — открой Консоль, там видны ошибки скриптов.
Ты прошёл все 50 уроков и теперь умеешь строить сцены, писать скрипты, оживлять врагов, делать квесты, гонки и даже мультиплеер. Это настоящие навыки создателя игр. Впереди — только твоя фантазия. Придумывай, строй, показывай друзьям. Удачи, и пусть твои игры будут лучшими!
> ), }, // ════════════════════════════════════════════════════ // РАЗБОР ИГР · Двор с табличкой (оригинал id=1991) // ════════════════════════════════════════════════════ 'guide-dvor': { body: ( <>Маленький уютный двор: зелёный газон, деревянный забор, деревья и большая 3D-табличка в центре. По табличке можно нажать мышкой прямо в игре — и что-то произойдёт. А ещё двор учит главному: как крутить камеру вокруг героя, как в настоящем Roblox.
game.billboard.onClick).Табличка — это особый примитив «3D-табличка» (биллборд). У неё есть кнопка, на которую можно нажимать.
primitive:41 (число у тебя может
быть другое).
Теперь сделаем, чтобы по нажатию на табличку менялся цвет неба.
{`// === ДВОР С ТАБЛИЧКОЙ — главный скрипт ===
// Подписываемся на клик по кнопке таблички.
// 'primitive:41' — номер твоей таблички, 'buy' — её кнопка.
game.billboard.onClick('primitive:41', 'buy', () => {
game.environment.setSkyColor('#88c0ff'); // небо стало голубым
game.log('По табличке нажали!');
});`}
game.billboard.onClick(номер, кнопка, функция) —
«когда нажмут на эту кнопку, выполни функцию». Внутри мы
меняем цвет неба командой
game.environment.setSkyColor.
Витрина магазина, как в играх-кликерах. 3D-мира почти нет — весь экран это интерфейс: счётчик монет и яркие кнопки. И все кнопки живые: пульсируют, крутятся, увеличиваются при наведении и «вдавливаются» при нажатии.
game.gui.tween);game.broadcast и game.onMessage).18 — это 18% экрана.
Повесим на кнопку «X2 денег» скрипт: при клике она эффектно крутится и на 5 секунд удваивает награду.
{`// === Скрипт кнопки X2 ===
game.self.onClick(() => {
// сначала вернём поворот в 0, потом плавно крутанём на 360°
game.gui.update(game.self, { rotation: 0 });
game.gui.tween(game.self, { rotation: 360 }, { duration: 0.5 });
// включаем множитель x2 на 5 секунд
game.broadcast('multiplier_set', 2);
game.after(5, () => game.broadcast('multiplier_set', 1));
});`}
game.gui.tween(объект, что-меняем, как-долго) —
это и есть плавная анимация. Без неё кнопка прыгнула бы резко,
а с твином крутится гладко, как в дорогих играх.
{`// === Витрина GUI — главный скрипт ===
let coins = 0; // монеты
let multiplier = 1; // множитель награды
game.hud.setHotbarVisible(false); // в этой игре инвентарь не нужен
// Кнопки шлют 'coin_add' — добавляем монеты с учётом множителя.
game.onMessage('coin_add', (amount) => {
coins = coins + amount * multiplier;
game.gui.update('hud_coins', { text: 'Монеты: ' + coins });
});
// Кнопка X2 шлёт 'multiplier_set' — меняем множитель.
game.onMessage('multiplier_set', (m) => { multiplier = m; });`}
Маленькое приключение. Лесная поляна с каменными руинами, светящийся сундук в центре и страж рядом. Игра показывает модальные сцены — это когда мир затемняется, всё замирает, и на экране появляется что-то важное: диалог, выбор приза или большая надпись.
game.modal.dialog);game.modal.open);game.modal.confirmation);game.modal.lootbox);В обычном Roblox такую сцену собирают из 5-6 кусков вручную. У нас — одна команда. Главный скрипт следит за расстоянием до стража и запускает диалог.
{`// === ТАЙНА СУНДУКА — главный скрипт ===
game.hud.setHotbarVisible(false);
let phase = 'start'; // этап квеста
game.onTick((dt) => {
if (game.modal.isOpen()) return; // во время модала ничего не триггерим
const p = game.player.position;
// расстояние до стража (он в точке -6, 4)
if (phase === 'start') {
const d = Math.hypot(p.x - (-6), p.z - 4);
if (d < 4) {
phase = 'talked';
game.modal.dialog('Страж Руин', [
'Стой, путник. Это место хранит тайну веков...',
'В центре руин дремлет Старый сундук.',
'Он заперт магией. Подойди — и он сам откроется.',
], () => game.log('Иди к сундуку!'));
}
}
});`}
game.onTick — сцена «появляется» не
мгновенно, и в первую секунду объект ещё не найден.
Подошёл к сундуку — затемняем мир, но сундук оставляем ярким (вокруг него прожектор), камера сама подлетает.
{`// (продолжение onTick) — подошёл к сундуку
if (phase === 'talked') {
const d = Math.hypot(p.x - 0, p.z - (-7));
if (d < 5) {
phase = 'open';
const chest = game.scene.findOne('chest');
game.modal.open({
darken: 0.72, // затемнить мир на 72%
spotlights: [chest], // сундук остаётся ярким (прожектор)
cameraOverride: { target: chest, distance: 9, duration: 0.7 },
blockInput: true, // заблокировать управление
content: { elements: [
{ kind: 'text', x: 50, y: 20, text: 'Старый сундук',
textSize: 50, textColor: '#ffd700', animationPreset: 'glow' },
]},
});
}
}`}
Готовые сцены, которые вызываются одной строкой:
game.modal.confirmation('Открыть?', 'текст', наДа, наНет) — вопрос Да/Нет;game.modal.lootbox([призы], выбор) — карточки призов;game.modal.bossIntro(имя, hp, [враги]) — заставка перед боссом.{`// Лутбокс — четыре приза, игрок выбирает один
game.modal.lootbox([
{ name: 'Меч зари', icon: '⚔', color: '#c0392b' },
{ name: 'Щит луны', icon: '🛡', color: '#2c5fb3' },
{ name: 'Кошель злата', icon: '💰', color: '#b8860b' },
{ name: 'Перо феникса', icon: '🔥', color: '#8e44ad' },
], (item) => {
game.log('Игрок выбрал: ' + item.name);
});`}
Самая весёлая игра! Ты начинаешь её в виде... пончика. По парку стоят таблички: нажми на кнопку таблички — и твой герой превращается в бургер, болид, пришельца, рыбу или человечка. А по клавише B открывается магазин скинов.
game.player.setSkin);primitive:10 и т.д.).
Главный скрипт подписывается на клик по каждой табличке и меняет скин одной командой.
{`// === ПАРК ЖИВОТНЫХ — главный скрипт ===
game.player.setSkinCoins(200); // 200 рубликов на магазин
// Каждая табличка при клике надевает свой скин.
game.billboard.onClick('primitive:10', 'buy', () => game.player.setSkin('burger'));
game.billboard.onClick('primitive:11', 'buy', () => game.player.setSkin('car-race'));
game.billboard.onClick('primitive:12', 'buy', () => game.player.setSkin('alien'));
game.billboard.onClick('primitive:13', 'buy', () => game.player.setSkin('fish'));
game.billboard.onClick('primitive:14', 'buy', () => game.player.setSkin('character-a'));
// Узнать, когда скин сменился:
game.player.onSkinChange((newSkin) => {
game.log('Теперь я: ' + newSkin);
});`}
game.player.setSkin('burger') — одна строчка
меняет всё тело героя на новую модель. Имена скинов
(burger, car-race, alien…) видны в окне «Скины».
Магазин уже встроен — мы включили его галочкой в окне «Скины». В игре он открывается клавишей B. Командами скинами тоже можно управлять:
{`game.player.unlockSkin('alien'); // открыть скин игроку
game.player.openSkinShop(); // открыть магазин из кода
game.player.setSkinCoins(500); // задать баланс рубликов`}
.glb тоже можно
загрузить в окне «Скины».
Простой туториал-уровень: ровный газон и три цели — красный куб, синяя сфера и золотой сундук. Над целью парит светящаяся стрелка-указатель «иди сюда», а к ней по земле бежит дорожка из шевронов — точь-в-точь как маркеры заданий в Roblox. Дошёл до цели — стрелка сама перепрыгивает на следующую и меняет цвет.
guide (красная), quest (жёлтая),
gift (радужная); red-cube (куб), blue-sphere (сфера),
gold-chest (куб). Имя — это как раз то, по чему
скрипт найдёт объект.
Стрелку создаёт одна команда. Важно: ищем цель не сразу,
а через game.after — на старте объекты сцены ещё
могут быть не готовы.
{`// === ТУТОРИАЛ — СОБЕРИ МОНЕТКИ — главный скрипт ===
const targets = ['red-cube', 'blue-sphere', 'gold-chest'];
const presets = ['guide', 'quest', 'gift']; // цвета стрелки по очереди
let step = 0;
let arrow = null;
game.ui.set('hint', 'Иди за стрелкой к цели!', { x: 50, y: 6, anchor: 'top' });
// Через 0.4 сек — создаём стрелку от игрока к первой цели.
game.after(0.4, () => {
const first = game.scene.findOne(targets[0]);
arrow = game.fx.pointer({ from: 'player', to: first, preset: 'guide' });
});`}
game.fx.pointer({ from, to, preset }) —
from: 'player' значит «от игрока»,
to — объект-цель (нашли через
findOne), preset — стиль.
На каждую цель вешаем onTouch: дошёл — берём
следующую и перенацеливаем ту же стрелку через
setTarget, а стиль меняем через
update.
{`for (let i = 0; i < targets.length; i++) {
const obj = game.scene.findOne(targets[i]);
if (!obj) continue;
obj.onTouch(() => {
if (i !== step) return; // только текущая по порядку цель
step++;
if (step >= targets.length) {
if (arrow) arrow.remove(); // все цели собраны — убираем стрелку
game.ui.set('hint', 'Молодец! Все цели собраны!', { x: 50, y: 6, anchor: 'top' });
return;
}
const next = game.scene.findOne(targets[step]);
arrow.setTarget(next); // стрелка теперь ведёт к следующей
arrow.update({ preset: presets[step] }); // и меняет цвет
});
}`}
game.fx.pointer с
texture: 'lightning', color: '#22ff66'
и curved: true — получится изогнутая зелёная
молниевая стрелка.
Лего-полигон в стиле Roblox: зелёный пол и оранжевая стена из окрашиваемых блоков, жёлтая ступенька, синий куб и фиолетовые кубики — все с узнаваемой текстурой «studs» (лего-кружки). В углу — готовый лего-сет: деревья, дом и машина. Один и тот же материал, любой цвет — как настоящий конструктор.
{`// Окрашиваемый блок — пол:
for (let x = -15; x < 15; x++)
for (let z = -15; z < 15; z++)
game.scene.spawn('block:studs-block', { x, y: 0, z, color: '#5cba35' });
// Примитив с лего-текстурой:
game.scene.spawn('primitive:cube', {
x: 0, y: 1, z: 0, sx: 2, sy: 2, sz: 2,
color: '#3a7aff', material: 'studs',
});
// Готовая модель из лего-сета:
game.scene.spawn('model:lego-house-small', { x: 10, y: 0, z: -10 });
// Сменить цвет блока на лету:
game.scene.setColor('block:0,0,0', '#ff0000');`}
Часовая башня с живыми надписями прямо в 3D и витриной- лутбоксом: над башней — таймер обратного отсчёта в жёлто-синей рамке (как в Roblox); ряд из трёх подиумов, на каждом парит и вращается предмет (меч, кубок, ключ), а перед ним — наклонная табличка-ценник с названием; счётчик монет «1 230 рубликов» с золотой монетой (клик +10); над зомби — полоса HP. Все надписи обновляются сами, без мигания.
interval сек, текстура перерисовывается только
когда текст реально изменился (диф-чек);onEnd-колбэком;'front'/'top'/...): наклоняешь сам примитив —
текст лежит в его плоскости и наклонён вместе с ним;gameui, boss-hp,
reward (золото), warning, plain;{'… '};time (00:15:59),
money («1 334 рублика» — разделитель + склонение);hh:mm:ss, стиль gameui.
Жми Play — цифры пошли вниз.
Правильная логика — наклоняешь сам примитив-планшет, а текст
крепишь к его передней грани через attachFace: 'front'.
Текст ляжет ТОЧНО в плоскость планшета и наклонится вместе с ним —
как ценник на витрине. Размер планшета сделай чуть больше текста,
чтобы надпись не вылезала за края.
{`// Планшет наклонён в редакторе (поворот по X ≈ -29°, верх назад).
// Табличка лежит ПЛОСКО на его передней грани (БЕЗ отдельного наклона):
const plate = game.scene.findOne('Планшет1');
game.scene.bindLabel(plate, () => 'Золотой кубок',
{ preset: 'gameui', size: 0.8, attachFace: 'front' });
// Предмет на подиуме — парит и вращается:
const obj = game.scene.findOne('Предмет1');
const x = obj.position.x, z = obj.position.z;
let t = 0;
game.onTick((dt) => {
t += dt;
obj.move(x, 1.9 + Math.sin(t * 2) * 0.18, z); // парение
obj.rotate(t * 1.2); // вращение вокруг Y
});
// Клик по предмету — "взять":
obj.onClick(() => game.ui.set('grab', 'Ты взял: Золотой кубок!',
{ x: 50, y: 14, anchor: 'top', color: '#ffd23a', size: 22 }));`}
{`// Таймер над башней (на передней грани верхнего яруса):
const endTs = Date.now() + 16 * 60 * 1000;
game.scene.bindTimer(game.scene.findOne('ВерхБашни'), endTs, {
prefix: 'Сбросится через ', format: 'hh:mm:ss', preset: 'gameui', attachFace: 'front',
});
// Счётчик монет (формат с разделителем и склонением), клик +10:
const plate = game.scene.findOne('ПланшетМонет');
let coins = 1230;
game.scene.bindLabel(plate, () => game.format.money(coins),
{ preset: 'reward', attachFace: 'front' });
game.scene.findOne('МонетаСчёт').onClick(() => { coins += 10; });
// HP над зомби — billboard (всегда к камере), меняется по клику:
const zombie = game.scene.findOne('Зомби');
let hp = 100;
game.scene.bindLabel(zombie, () => 'Зомби HP: ' + hp + '/100', { preset: 'boss-hp' });
zombie.onClick(() => { hp = Math.max(0, hp - 10); });`}
game.after(0.3, () => {'{ … }'}).
Без attachFace плашка висит билбордом над верхом
объекта (как HP-полоса) — это удобно для NPC.
Диф-чек: bindLabel перерисовывает текстуру
только когда строка изменилась — таймер обновляется раз в секунду,
а не каждый кадр. Привязка сама отменяется при
scene.delete — утечек нет.
attachFace: 'front', а предмет заставь парить и
крутиться через obj.move + obj.rotate.
Маленький tycoon-завод на воксельном острове: трава, холмы, деревья, пруд. Снизу — магазин-инвентарь из трёх слотов (ящик, дерево, печь). Кликаешь слот → за курсором летит полупрозрачная тень предмета, повторяющая его форму. ЛКМ ставит предмет на свой участок (можно класть стопкой), R или колесо — поворот, ПКМ/Esc — отмена. Денег не хватает → слот серый, поставить нельзя. Это базовый gameplay-loop любой топ-игры: купи → поставь → развивай (Pet Simulator, Lumber Tycoon, Build A Boat).
onSlotClick(item). Слот сам сереет, если валюты мало;
game.inventoryUi.create рисует панель слотов. Каждый
слот — это товар: ключ, название, иконка, цена и тип модели.
В onSlotClick запускаем расстановку этого товара.
{`let rubles = 1000;
function wallet() {
game.ui.set('wallet', game.format.money(rubles) + ' рубликов',
{ x: 50, y: 6, anchor: 'top', color: '#ffd23a', size: 24 });
// Обновляем баланс магазина — слоты дороже денег станут серыми:
game.inventoryUi.setBalance('rubles', rubles);
}
wallet();
game.after(0.4, () => {
game.inventoryUi.create({
items: [
{ key: 'crate', name: 'Ящик', icon: 'crate', cost: 50, modelType: 'user:444' },
{ key: 'tree', name: 'Дерево', icon: 'plant', cost: 100, modelType: 'user:445' },
{ key: 'oven', name: 'Печь', icon: 'oven', cost: 500, modelType: 'user:446' },
],
position: 'bottom', showCost: true, showCurrency: 'rubles',
onSlotClick: (item) => startPlacing(item.key), // ← см. Шаг 2
});
wallet();
});`}
game.placement.start создаёт полупрозрачную тень и
включает режим: тень едет за курсором, снаппится к сетке, краснеет
вне зоны. previewScale для куба — размер в юнитах, а
для воксельной модели задавай modelScale = масштаб
модели — тогда тень будет точной формой предмета.
{`const SHOP = {
crate: { id: 'user:444', cost: 50, scale: 1.6 },
tree: { id: 'user:445', cost: 100, scale: 7.0 },
oven: { id: 'user:446', cost: 500, scale: 2.0 },
};
function startPlacing(key) {
const s = SHOP[key];
game.placement.start(key, {
previewType: s.id, // 'user:' — воксельная модель
modelScale: s.scale, // тень той же формы и размера, что предмет
surfaceMode: 'ground', // ставим на землю ИЛИ на верх другого объекта (стопка)
grid: 0.5, // снап к полусетке
cost: s.cost, currency: 'rubles', // нет денег → ставить нельзя
targetZone: game.scene.findOne('player-plot'), // свой участок
chainPlace: true, // ставить подряд, не выходя из режима
previewPulse: true, // тень слегка пульсирует
});
}`}
chainPlace: true
после установки режим не закрывается — удобно строить рядами.
Когда игрок жмёт ЛКМ на валидном месте, движок зовёт
onPlace с позицией и поворотом. Здесь создаём
настоящий объект (тень — лишь подсказка) и списываем монеты.
{`game.placement.onPlace(({ itemKey, position, rotationY }) => {
const s = SHOP[itemKey];
game.scene.spawn(s.id, {
x: position.x, y: position.y, z: position.z,
rotationY: rotationY, scale: s.scale,
name: itemKey + '_' + Date.now(),
});
rubles -= s.cost;
wallet(); // обновили счётчик и серость слотов
});
// (необязательно) реакция на отмену:
game.placement.onCancel(() => game.ui.set('hint', '', {}));`}
Тень ≠ предмет. Пока ты в режиме расстановки — на сцене
только полупрозрачная подсказка. Реальный объект появляется
один раз в onPlace. Поэтому ничего не дублируется,
а всё, что наспавнено за игру, удаляется при выходе (Esc/Стоп) —
игровая сессия не «протекает» в сохранение. Деньги списываются только
при успешной установке, а недоступные по цене слоты серые — в минус
уйти нельзя.
user:448) со своей ценой. Сделай ему дорогую стоимость
и проверь: пока денег мало — слот серый и не ставится, а как накопишь —
станет активным. Попробуй построить из ящиков башню (стопкой).
Три локации в одном проекте, между ними — красивая загрузка. Игрок в гараже у жёлтого такси жмёт «Поехать в город» → весь экран плавно затемняется, в центре снимок сцены, жёлтый прогресс-бар заполняется за 4 секунды, крупно процент, кнопка «Пропустить» и спиннер «Загрузка». Через 4 секунды экран исчезает — игрок уже в городе с высотками и закатным небом. В городе кнопка «Магазин» делает короткий переход внутрь закрытого магазина (ряды стеллажей, кассы), а «Назад» возвращает на улицу. Это та самая «загрузка между мирами» из больших игр (Taxi Boss, Brookhaven, Jailbreak) — несколько миров без отдельных «уровней».
duration секунд, сам скрыл.
Возвращает Promise — пишешь await и
продолжаешь код уже «на новом уровне»;setProgress(0..1), setText(),
close() — для реальной загрузки ресурсов;skipButton) и
спиннер (spinner) — включаются одной опцией;
В Настройки → Экран загрузки один раз задаёшь логотип
игры, цвет акцента (бар и кнопка) и галочки «спиннер» /
«кнопка Пропустить» по умолчанию. Дальше любой game.loading
в этой игре берёт их сам — не повторяешь стиль в каждом вызове.
Делаем кнопку и вешаем на неё переход. await
game.loading.transition(...) «замораживает» код, пока крутится
загрузка, и продолжается, когда она закончилась (или игрок нажал
«Пропустить»). После — телепорт и смена окружения: игрок «оказался» в
новом мире.
{`game.gui.create('button', {
id: 'btn_go', x: 50, y: 92, w: 26, h: 9, anchor: 'center',
text: 'Поехать в город', textColor: '#3a2a00', textSize: 20, fontWeight: 800,
bgGradient: { stops: ['#ffd23a', '#e0a000'], angle: 90 }, borderRadius: 12,
});
game.gui.onClick('btn_go', async () => {
game.gui.remove('btn_go');
await game.loading.transition({
cover: { sceneSnapshot: true }, // снимок текущей сцены как картинка
duration: 4, // бар заполняется 4 секунды
text: 'Едем в центр города...',
skipButton: true, // можно пропустить ожидание
spinner: true, // спиннер «Загрузка» справа
});
// Этот код выполнится ПОСЛЕ загрузки (экран уже скрыт):
game.player.teleport(100, 2, 100); // телепорт в город
game.scene.environment = 'sunset'; // закатное небо
});`}
transition — «фейковый» прогресс на заданное время (для
красивого перехода). Точно так же сделана кнопка «Магазин»:
переход 1.5с → teleport внутрь закрытого
зала-магазина (отдельная локация со стеллажами и кассами) → кнопка
«Назад» возвращает в город. Для реальной загрузки ресурсов есть
show + setProgress — см. Шаг 3.
Если грузишь много объектов и хочешь показать настоящий
прогресс — открой экран через show и двигай бар сам
через setProgress. Закрой через close().
{`const lo = game.loading.show({ progressBar: true, spinner: true });
const total = 10;
let i = 0;
const step = () => {
i++;
// ... подгрузить i-й кусок мира ...
lo.setProgress(i / total); // двигаем бар вручную
if (i < total) game.after(0.2, step); // следующий шаг через 0.2с
else lo.close(); // всё загружено — спрятать экран
};
step();`}
Один проект — три мира (гараж · город · интерьер магазина),
собранные в разных углах сцены, а переключение между ними прячется за
красивым экраном загрузки. Игрок не видит «телепорт рывком» — видит
плавную загрузку, как в больших играх. А await делает код
линейным: «показать загрузку → дождаться → телепортировать → сменить
небо». Так из одного проекта получается целая игра с локациями, без
отдельных уровней-файлов.
x = -100), сделай кнопку «В парк»
и переход на неё через loading.transition (свой текст и
duration). Не забудь кнопку «Назад», которая возвращает в
город. Подсказка: каждый переход = await transition →
teleport(...) → environment = '...'.
Главное меню как в топ-играх Roblox. Игрок попадает в тёмный гараж, где камера кинематографично облетает премиум-машину (модель из набора Kenney). Сверху — логотип игры, справа — список обновлений (патч-ноуты), снизу — большая жёлтая кнопка «ИГРАТЬ», играет фоновая музыка. Управление заблокировано — игрок только смотрит. Клик «ИГРАТЬ» → плавная загрузка → ты в самой игре. Первые 5 секунд решают, останется игрок или уйдёт — поэтому красивое меню = лицо игры.
setPatchNotes;
Весь экран строится одним game.mainMenu.show. Камера в
режиме cinematic летит по точкам waypoints
вокруг машины (центр target). Каждая точка — позиция
камеры + куда она смотрит.
{`game.mainMenu.show({
title: 'ГАРАЖ БОССА',
camera: {
mode: 'cinematic', duration: 16,
waypoints: [
{ position: { x: 6, y: 2.6, z: 5.5 }, target: { x: 0, y: 1.3, z: 0 } },
{ position: { x: 0, y: 1.6, z: 6 }, target: { x: 0, y: 1.3, z: 0 } },
{ position: { x: -6, y: 3.2, z: -4.5 },target: { x: 0, y: 1.3, z: 0 } },
{ position: { x: 6, y: 2.6, z: 5.5 }, target: { x: 0, y: 1.3, z: 0 } }, // петля
],
},
patchNotes: {
version: '3.10', title: 'ГАРАЖ БОССА',
items: ['Новая система контрактов', '2 ремоделинга', '2 новых обвеса',
'Улучшенные звуки', 'Багфиксы и многое другое...'],
},
music: 'epoch_01_main',
playButtonText: 'ИГРАТЬ',
onPlay: onPlay, // ← см. Шаг 2
});`}
orbit (круговой облёт: center/radius/height),
static (одна точка), preset-cuts (резкие
смены ракурса, как в трейлере).
По кнопке прячем меню (это снимает блок управления, музыку и камеру), показываем загрузку (задача 12) и телепортируем игрока в игровую зону.
{`async function onPlay() {
game.mainMenu.hide(); // убрать меню, вернуть управление
await game.loading.transition({ // красивая загрузка (задача 12)
cover: { sceneSnapshot: true }, duration: 1.5, text: 'Загружаем мир...',
});
game.player.teleport(0, 2, 100); // в игровую зону (поляна)
game.scene.environment = 'day'; // дневное небо
}`}
Меню — отдельная зона проекта (гараж-витрина), игра — другая
(поляна). До клика ИГРАТЬ человек видит живую кинематографичную
картинку, а не «прототип со спавном». Камера, патч-ноуты и музыка
создают ощущение большой игры. А весь переход — это просто
hide → transition → teleport.
orbit (круговой облёт):
camera: {`{ mode: 'orbit', center: { x:0, y:1, z:0 }, radius: 6, height: 2.5, duration: 12 }`}.
И обнови патч-ноуты под свою игру через
game.mainMenu.setPatchNotes(...).
Настоящие машины, на которых можно ездить. Подходишь к автомобилю → над ним появляется подсказка «[F] Enter», держишь F → садишься за руль. WASD рулят (газ/тормоз/задний ход + повороты), камера плавно следует за машиной, снизу — спидометр с передачей (D/R/N). V меняет ракурс камеры, E — выйти. Машина сталкивается со стенами и столбами. Это основа таксопарков, гонок, доставки — 30% жанров Roblox держатся на транспорте.
Одна строка создаёт готовый к езде автомобиль. model —
одна из встроенных 3D-моделей (car-taxi / car-sedan / car-truck /
car-suv-luxury и др.), name показывается в подсказке F.
{`game.scene.spawn('vehicle:car', {
x: -28, y: 0.5, z: -22, rotationY: 0,
model: 'car-taxi', color: '#ffd23a', name: 'Natsune Alltima',
params: { maxSpeed: 14, turnSpeed: 1.8, enginePower: 16, brake: 28 },
});`}
Глобальные события onVehicleEnter / onVehicleExit
дают зацепку для логики игры — например, показать инструкцию или
начать отсчёт заказа такси.
{`game.onVehicleEnter((vehicleRef) => {
game.ui.set('hint', 'Поехали! Отвези клиента.', { x: 50, y: 88, anchor: 'bottom' });
});
game.onVehicleExit((vehicleRef) => {
game.ui.set('hint', 'Ты вышел из машины.', { x: 50, y: 88, anchor: 'bottom' });
});`}
Любое взаимодействие можно сделать «по удержанию» — игрок должен
подержать клавишу, а не случайно ткнуть. Полезно для важных действий
(подобрать клиента, продать машину). Опция holdDuration в
onInteract:
{`game.self.onInteract(() => {
game.ui.set('hint', 'Клиент сел!', { x: 50, y: 88, anchor: 'bottom' });
}, { text: 'Подобрать клиента', distance: 5, key: 'f', holdDuration: 0.5 });`}
Тебе не нужно писать «низкоуровневую» возню — движок берёт её на себя:
Машина — отдельная подсистема: пока ты за рулём, WASD управляют автомобилем, а не ходьбой, и камера автоматически переключается на «погоню за машиной». Выход возвращает обычное управление. На этой базе строятся целые игры: такси, гонки, доставка, мафия-симулятор.
model: 'car-suv-luxury')
с другим цветом и параметрами (быстрее: maxSpeed: 20).
Поставь рядом столб (game.scene.spawn('primitive:cylinder', ...))
и проверь — машина останавливается, столб стоит.
Красивое небо в одну строку. Вместо плоского цветного фона — градиентный купол с солнцем, плывущими облаками, дымкой у горизонта и далёкими горами (low-poly стиль, как в топовых Roblox-играх). Небо меняется кнопками: день, закат, звёздная ночь, космос — с плавным переходом за пару секунд. И главное: небо — это единый источник света: меняешь пресет → вместе с небом меняется и освещение всей сцены (на закате теплеет, ночью темнеет).
Самое простое — выбрать пресет. Доступны:
clear-summer-day, lowpoly-roblox,
cloudy, sunset, starry-night,
space.
{`// Голубое low-poly небо с облаками, дымкой и горами (как на скрине):
game.scene.setSkybox({ preset: 'lowpoly-roblox' });
game.scene.setClouds({ enabled: true, cover: 0.45, speed: 0.014 });
game.scene.setFog({ color: '#e2eef7', density: 0.005 });`}
skybox.fadeTo переводит небо к новому пресету за указанное
число секунд — цвета купола, солнце, облака, туман и свет сцены
меняются плавно. Удобно вешать на кнопки или события.
{`game.gui.onClick('btn-sunset', () => game.scene.skybox.fadeTo({ preset: 'sunset' }, 2));
game.gui.onClick('btn-night', () => game.scene.skybox.fadeTo({ preset: 'starry-night' }, 2));
game.gui.onClick('btn-space', () => game.scene.skybox.fadeTo({ preset: 'space' }, 2));`}
Можно не брать пресет, а задать цвета купола вручную — верх, низ, горизонт и солнце.
{`game.scene.setSkybox({
mode: 'gradient',
topColor: '#3d7fe0', // зенит
bottomColor: '#dcebf7', // у земли
horizonColor: '#bcd9f2', // линия горизонта
sunDirection: { x: 0.3, y: 0.85, z: 0.4 },
sunColor: '#fff6d8',
sunSize: 0.035,
});`}
Небо — половина визуального впечатления от мира. С плоским фоном все игры выглядят одинаково и дёшево; с кастомным небом — атмосферно и «дорого». А связка неба с освещением даёт бесплатный приём: смена времени суток одной строкой мгновенно меняет настроение всей сцены.
fadeTo между 'clear-summer-day' и
'starry-night'. Добавь облака погуще на день и убери на ночь.
Игра «собери монеты» с двумя системами удержания, как в Roblox: таблица лидеров справа-сверху (монеты, время, уровень) и достижения — всплывающие награды с редкостью, звуком и страницей-витриной. Прогресс сохраняется в базе — закрыл игру, вернулся завтра, а монеты и открытые ачивки на месте.
Объяви столбцы. Первый primary: true — по нему сортируются
игроки в топе. Дальше меняй значения через me.add / me.set.
{`game.leaderstats.define('Монеты', { initial: 0, format: 'number', icon: '🪙', color: '#ffd23a', primary: true });
game.leaderstats.define('Время', { initial: 0, format: 'time', icon: '⏱', color: '#7fd0ff' });
game.leaderstats.define('Уровень', { initial: 1, format: 'number', icon: '⭐', color: '#b48bff' });
// Время идёт само, монеты — за подбор
game.every(1, () => game.leaderstats.me.add('Время', 1));
game.leaderstats.me.add('Монеты', 1);`}
Объяви список достижений с редкостью (common / rare / epic /
legendary — разный цвет плашки и звук). Выдавай явно через
unlock или автоматически через bindToStat.
{`game.achievements.define([
{ id:'first_coin', name:'Первая монета', description:'Подобрать монету', icon:'🪙', rarity:'common', points:5 },
{ id:'fifty_coins', name:'Полная сумка', description:'Собрать 50 монет', icon:'💰', rarity:'rare', points:25 },
]);
// Авто-награда: как только Монеты >= 50 — плашка «Полная сумка» сама выедет
game.achievements.bindToStat('fifty_coins', 'Монеты', { gte: 50 });
// Явная выдача (на первой монете)
game.achievements.unlock('first_coin');`}
Кубок слева-снизу открывает страницу «Мои достижения»: открытые — цветные с рамкой по редкости, закрытые — серые с замком, скрытые — «?», сверху прогресс-бар «N из M (очки)».
Лидерборды и достижения — главный механизм удержания: ребёнок возвращается в игру за новым рекордом и новой ачивкой. Это основа симуляторов, ферм и PvP — в Roblox столбец «Coins / Wins / Level» есть почти в каждой игре.
'speedrun', которое
выдаётся через bindToStat('Время', {'{'} lte: 30 {'}'}),
если собрать все монеты быстрее 30 секунд.
Мини-шутер: волны зомби бегут к игроку, ты отстреливаешь их из бластера, а над каждой целью всплывает облачко урона — как в Roblox-RPG (Pet Sim, Anime Adventures). Зомби гибнут, счётчик растёт, волны усиливаются.
Две строки превращают игру в шутер с фидбеком урона: выдаём бластер и включаем авто-floater — теперь любой урон по NPC сам рисует «-N» над целью, вручную вызывать ничего не надо.
{`game.player.giveTool('blaster-blaster-a', { equip: true }); // бластер в руки
game.fx.autoMobFloaters(true); // облачко урона над любым мобом при попадании`}
{`function spawnWave(n){
const pl = game.player.position;
for (let i = 0; i < n; i++){
const a = (i / n) * Math.PI * 2;
const e = game.scene.spawnNpc('skin_retro-zombie', {
x: pl.x + Math.cos(a)*18, z: pl.z + Math.sin(a)*18,
name: 'Зомби', hp: 100, speed: 2.6,
});
if (e && e.follow) e.follow('player'); // зомби преследует игрока
}
}
game.after(1.5, () => spawnWave(5));
game.every(14, () => spawnWave(8));`}
Стрелять из бластера — ЛКМ. В режиме от 3-го лица пуля летит
туда, куда кликнул курсором. Попал по зомби → облачко
урона (благодаря autoMobFloaters), убил → засчитан.
Когда нужен полный контроль — рисуй цифру сам:
{`game.fx.damageFloater(pos, 25); // красный — обычный урон
game.fx.damageFloater(pos, 80, { isCrit: true }); // жёлтый, больше + подскок
game.fx.damageFloater(pos, 30, { isHeal: true }); // зелёный — лечение (+30)
game.fx.damageFloater(pos, 50, { isMana: true }); // синий — мана
game.fx.damageFloater(pos, 'Промах', { isMiss: true }); // серый текст`}
position — {'{x,y,z}'}, ссылка на объект или
'player'; value — число или строка.
{`// общий stackKey → удары сливаются в «-25 ×N» вместо кучи цифр
game.fx.damageFloater(enemy.position, 25, { stackKey: 'aoe_' + enemy.id });
// comicStyle → BAM! (>50), KAPOW! (>100), POW! (крит) на жёлтой звезде
game.fx.damageFloater(pos, 120, { comicStyle: true });`}
Без облачек урона стрельба ощущается «впустую». Это базовый
боевой фидбек: игрок видит, сколько нанёс, был ли крит, попал ли.
Связка бластер + autoMobFloaters + волны NPC — готовый
каркас любого шутера/выживания.
damageFloater(pos, 15, {'{'} color:
'#ff7a2a' {'}'}) каждые 0.5 сек 3 раза — эффект горения.
Или увеличь HP зомби и добавь крит каждый 5-й выстрел.