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 тоже можно
загрузить в окне «Скины».