Some checks failed
Задача 04 — модальные сцены (затемнение + GUI/3D-анимация + блок ввода): - engine/ModalManager.js (state-машина, fade, spotlight-проекция, HighlightLayer) - editor/ModalOverlay.jsx (CSS-overlay + mask-image для spotlight) - PlayerController.setInputBlocked/setCameraFrozen/captureCameraState/focusOnTarget - game.modal.open/close/update/isOpen/onClose + пресеты bossIntro/lootbox/dialog/confirmation - Фиксы ядра: клик GUI-кнопок (realId↔localId), modalOpened через globalEvent, guard от двойного срабатывания колбэков, кнопка ✓ на последней строке диалога Задача 07 — кастомные скины игрока + магазин: - non-humanoid скины (любая 3D-модель): загрузчик, процедурная анимация, настраиваемый AABB, центрирование через pivot-node - PlayerController.reloadSkin (смена скина в Play) - game.player.setSkin/unlockSkin/getAvailableSkins/onSkinChange/openSkinShop + локальная валюта - editor/SkinShopOverlay.jsx (встроенный магазин, клавиша B) + SkinManagerModal.jsx (вкладка «Скины») - сериализация scene.skins, кнопка «Скины» в тулбаре Вики — категория «Разбор готовых игр»: - docsGames.js: группа g5 + 4 карточки (Двор/Витрина GUI/Сундук/Парк) - docsLessons.jsx: 4 подробные статьи-урока для детей - «Открыть мою копию» создаёт копию проекта (оригинал не трогается) Адаптивная ширина кнопки billboard под длинный текст. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7785 lines
441 KiB
JavaScript
7785 lines
441 KiB
JavaScript
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: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Небольшая площадка, по которой раскиданы золотые монетки.
|
||
Игрок ходит и собирает их — за каждую монетку счёт растёт
|
||
на единицу. Когда собраны все 8 монеток — победа, летит
|
||
конфетти.
|
||
</p>
|
||
<Shot src="lesson1-result.png"
|
||
caption="Так выглядит готовая игра «Собери монетки»" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Событие касания</b> — как объект понимает, что игрок
|
||
его коснулся (<code>onTouch</code>);</li>
|
||
<li><b>Счётчик очков</b> — как считать собранное и показывать
|
||
число на экране (<code>game.ui.score</code>);</li>
|
||
<li><b>Удаление объекта</b> — как убрать собранную монетку
|
||
со сцены (<code>game.self.delete</code>);</li>
|
||
<li><b>Связь скриптов</b> — как монетки сообщают главному
|
||
скрипту, что их собрали.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Площадка</h3>
|
||
<p>
|
||
Сначала построим пол, по которому будет ходить игрок.
|
||
</p>
|
||
<Step n="1">
|
||
На вкладке <b>Главная</b> выбери инструмент
|
||
<kbd className="kbd">Блок</kbd>, в палитре слева — блок
|
||
<b> травы</b>.
|
||
</Step>
|
||
<Step n="2">
|
||
Кликай по сцене и собери квадратную площадку примерно
|
||
14×14 блоков. Это игровое поле.
|
||
</Step>
|
||
<Step n="3">
|
||
По краю площадки поставь стенку из камня в один блок
|
||
высотой — чтобы игрок не убежал за край и не упал.
|
||
</Step>
|
||
<Shot src="lesson1-scene.png"
|
||
caption="Площадка из травы с каменным бортиком" />
|
||
<Note>
|
||
Не обязательно делать ровно 14×14 — главное, чтобы было
|
||
где разгуляться. Бортик можно сделать и повыше.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 2. Монетки</h3>
|
||
<p>
|
||
Монетка — это <b>примитив-сфера</b> жёлтого цвета.
|
||
Поставим 8 штук.
|
||
</p>
|
||
<Step n="1">
|
||
Выбери инструмент <kbd className="kbd">Примитив</kbd>,
|
||
в палитре — <b>Сфера</b>.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь сферу на площадку. Выдели её и в инспекторе
|
||
справа задай: <b>размер</b> 0.6 по всем осям (монетка
|
||
небольшая), <b>цвет</b> жёлтый <code>#ffd700</code>,
|
||
<b> материал</b> «Неон» — чтобы монетка светилась.
|
||
</Step>
|
||
<Step n="3">
|
||
Подними монетку чуть над полом — высота Y около 1.2,
|
||
чтобы игрок «врезался» в неё телом.
|
||
</Step>
|
||
<Step n="4">
|
||
В инспекторе <b>выключи «Столкновение»</b> — тогда игрок
|
||
проходит сквозь монетку, а не упирается в неё. Касание
|
||
мы поймаем скриптом.
|
||
</Step>
|
||
<Step n="5">
|
||
Дай монетке понятное имя — например «Монетка_1». Потом
|
||
выдели её и нажми <kbd className="kbd">Ctrl</kbd>+<kbd className="kbd">D</kbd>,
|
||
чтобы дублировать. Расставь 8 монеток по площадке.
|
||
</Step>
|
||
<Shot src="lesson1-coins.png"
|
||
caption="Восемь жёлтых монеток расставлены по полю" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт считает монетки и проверяет победу.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «СОБЕРИ МОНЕТКИ» — главный скрипт ===
|
||
// Этот скрипт глобальный: считает собранные монетки и проверяет победу.
|
||
|
||
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 });
|
||
}
|
||
});`}</Code>
|
||
<p>
|
||
Разберём построчно:
|
||
</p>
|
||
<ul>
|
||
<li><code>let score = 0</code> — переменная-счётчик,
|
||
в начале монеток собрано 0;</li>
|
||
<li><code>game.ui.score = score</code> — выводит счёт
|
||
в угол экрана;</li>
|
||
<li><code>game.onMessage('coin', ...)</code> — подписка
|
||
на сообщение «coin». Каждый раз, когда монетка
|
||
пришлёт это сообщение, выполнится наша функция —
|
||
прибавит счёт;</li>
|
||
<li><code>if (score {'>'}= TOTAL)</code> — когда счёт дошёл
|
||
до 8, показываем победу и запускаем конфетти.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт монетки</h3>
|
||
<p>
|
||
Теперь повесим скрипт на <b>каждую</b> монетку. Он ловит
|
||
касание и сообщает главному скрипту: меня собрали.
|
||
</p>
|
||
<ScriptKind kind="object" on="каждую монетку" />
|
||
<Code>{`// === Скрипт монетки ===
|
||
// game.self — это сама монетка, на которой висит скрипт.
|
||
|
||
game.self.onTouch(() => {
|
||
// игрок коснулся монетки — сообщаем главному скрипту
|
||
game.broadcast('coin');
|
||
game.self.delete(); // монетка исчезает со сцены
|
||
});`}</Code>
|
||
<p>
|
||
Что происходит: <code>onTouch</code> срабатывает, когда
|
||
игрок дотронулся до монетки. Внутри мы шлём
|
||
<code> game.broadcast('coin')</code> — главный скрипт
|
||
ловит это сообщение и прибавляет очко, а
|
||
<code> game.self.delete()</code> убирает монетку.
|
||
</p>
|
||
<Note>
|
||
Скрипт монетки <b>одинаковый</b> для всех 8 монеток.
|
||
Чтобы не писать его 8 раз: создай скрипт на первой монетке,
|
||
а когда дублируешь монетку (<kbd className="kbd">Ctrl</kbd>+<kbd className="kbd">D</kbd>) —
|
||
скрипт копируется вместе с ней.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 5. Проверка</h3>
|
||
<p>
|
||
Нажми <kbd className="kbd">Запустить</kbd>. Походи по
|
||
площадке и собери монетки:
|
||
</p>
|
||
<ul>
|
||
<li>при касании монетка исчезает и звенит;</li>
|
||
<li>счётчик в углу растёт: 1, 2, 3...;</li>
|
||
<li>когда собрал все 8 — надпись «Победа!» и конфетти.</li>
|
||
</ul>
|
||
<p>
|
||
Если что-то не работает — открой <b>Консоль</b> внизу
|
||
справа, там будут ошибки скриптов.
|
||
</p>
|
||
|
||
<Try>
|
||
добавь монеткам вращение — в главном скрипте через
|
||
<code> game.onTick</code> поворачивай все монетки, чтобы
|
||
они крутились и были заметнее. Поставь больше монеток.
|
||
Сделай одну монетку красной и «секретной» — за неё
|
||
давай сразу +5 очков.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 2 — «Прыгай по платформам»
|
||
// ════════════════════════════════════════════════════
|
||
'platform-jump': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Паркур: в воздухе висит дорожка из платформ с разрывами.
|
||
Игрок прыгает с платформы на платформу и должен добраться
|
||
до зелёной финишной площадки. Если упал вниз — игра
|
||
возвращает на старт.
|
||
</p>
|
||
<Shot src="lesson2-result.png"
|
||
caption="Паркур из платформ — допрыгай до зелёного финиша" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Примитивы-платформы</b> — как из растянутых кубов
|
||
строить дорожку для паркура;</li>
|
||
<li><b>Точка спавна</b> — откуда игрок начинает игру;</li>
|
||
<li><b>Падение и респаун</b> — как вернуть игрока на старт,
|
||
если он свалился (<code>onTick</code> +
|
||
<code> game.player.respawn</code>);</li>
|
||
<li><b>Финиш</b> — как сделать победную зону, на которую
|
||
нужно встать.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Старт и платформы</h3>
|
||
<Step n="1">
|
||
Поставь небольшую <b>стартовую площадку</b> из блоков
|
||
травы (4×4) — отсюда игрок начнёт.
|
||
</Step>
|
||
<Step n="2">
|
||
Возьми инструмент <kbd className="kbd">Примитив</kbd> →
|
||
<b> Куб</b>. Поставь куб перед стартом. В инспекторе задай
|
||
размер: ширина 2, <b>высота 0.5</b> (платформа плоская),
|
||
глубина 2. Цвет — коричневый.
|
||
</Step>
|
||
<Step n="3">
|
||
Подними платформу в воздух — высота Y около 1.5. Убедись,
|
||
что <b>«Столкновение» включено</b> (на платформу надо
|
||
вставать) и <b>«Закреплён» включён</b> (платформа не падает).
|
||
</Step>
|
||
<Step n="4">
|
||
Дублируй платформу (<kbd className="kbd">Ctrl</kbd>+<kbd className="kbd">D</kbd>)
|
||
и расставь 6-7 штук дорожкой вперёд — каждую следующую чуть
|
||
выше и со сдвигом, чтобы между ними был прыжок. Не делай
|
||
разрывы слишком большими — игрок должен допрыгнуть.
|
||
</Step>
|
||
<Shot src="lesson2-scene.png"
|
||
caption="Дорожка из платформ-кубов в воздухе" />
|
||
<Note>
|
||
Платформа — это куб с маленькой высотой (0.5). Так делают
|
||
все паркур-площадки: берёшь куб и сплющиваешь его по Y.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 2. Финиш</h3>
|
||
<Step n="1">
|
||
Поставь в конце дорожки ещё один куб, побольше (4×0.5×4) —
|
||
это финишная площадка.
|
||
</Step>
|
||
<Step n="2">
|
||
Покрась его в зелёный, материал «Неон» — чтобы финиш
|
||
светился и был заметен. Дай имя «Финиш».
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт следит за падением и обрабатывает победу.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПРЫГАЙ ПО ПЛАТФОРМАМ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Что тут важно:</p>
|
||
<ul>
|
||
<li><code>game.onTick(...)</code> — функция внутри
|
||
выполняется <b>каждый кадр</b>. Так мы постоянно
|
||
следим за игроком;</li>
|
||
<li><code>if (p.y {'<'} -3)</code> — если игрок опустился
|
||
ниже -3 по высоте, значит свалился с платформ;</li>
|
||
<li><code>game.player.respawn()</code> — возвращает игрока
|
||
на точку спавна;</li>
|
||
<li><code>won</code> — флажок, чтобы победа сработала
|
||
только один раз.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт финиша</h3>
|
||
<ScriptKind kind="object" on="зелёную финишную площадку" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
// Висит на невидимой зоне над зелёной площадкой.
|
||
// Игрок встал на площадку — его тело внутри зоны — победа.
|
||
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish'); // сообщаем главному скрипту о победе
|
||
});`}</Code>
|
||
<p>
|
||
Когда игрок касается финиша, скрипт шлёт
|
||
<code> game.broadcast('finish')</code>. Главный скрипт ловит
|
||
это сообщение через <code>game.onMessage('finish', ...)</code> —
|
||
там показывается «Победа» и летит конфетти. Скрипты в разных
|
||
«песочницах» общаются только сообщениями.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 5. Проверка</h3>
|
||
<ul>
|
||
<li>прыгай <kbd className="kbd">Space</kbd> с платформы
|
||
на платформу;</li>
|
||
<li>упал вниз — игра вернёт на старт со звуком;</li>
|
||
<li>встал на зелёную площадку — «Победа» и конфетти.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь движущуюся платформу — пусть одна платформа ездит
|
||
туда-сюда через <code>game.tween</code> с
|
||
<code> yoyo: true</code>. Сделай платформы поменьше —
|
||
прыгать будет сложнее. Поставь на пути монетки из урока 1.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 3 — «Не упади»
|
||
// ════════════════════════════════════════════════════
|
||
'dont-fall': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Дорожка из жёлтых плиток. Как только игрок встаёт на плитку,
|
||
она через секунду <b>исчезает</b>. Стоять нельзя — нужно
|
||
всё время бежать вперёд, к зелёному финишу. Зазевался —
|
||
плитка пропала, и ты падаешь вниз.
|
||
</p>
|
||
<Shot src="lesson3-result.png"
|
||
caption="Беги — плитки исчезают под ногами" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Таймер <code>game.after</code></b> — как выполнить
|
||
что-то не сразу, а через несколько секунд;</li>
|
||
<li><b>Исчезновение объекта</b> — как убрать плитку со сцены
|
||
с задержкой;</li>
|
||
<li><b>Флажок-защёлка</b> — как сделать, чтобы плитка
|
||
запускалась на исчезновение только один раз.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Дорожка из плиток</h3>
|
||
<Step n="1">
|
||
Поставь твёрдую <b>стартовую площадку</b> из блоков камня.
|
||
</Step>
|
||
<Step n="2">
|
||
Сделай плитку — примитив-куб размером 2×0.4×2, жёлтого цвета.
|
||
«Столкновение» и «Закреплён» — включены.
|
||
</Step>
|
||
<Step n="3">
|
||
Дублируй плитку и выложи дорожку вперёд — 12-14 плиток
|
||
в ряд, с небольшими промежутками. Можно зигзагом
|
||
влево-вправо, чтобы было интереснее.
|
||
</Step>
|
||
<Step n="4">
|
||
В конце поставь зелёную неоновую финишную площадку.
|
||
</Step>
|
||
<Shot src="lesson3-scene.png"
|
||
caption="Дорожка из исчезающих плиток" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>Следит за падением и победой — как в уроке 2.</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «НЕ УПАДИ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт исчезающей плитки</h3>
|
||
<p>
|
||
Самое главное — повесить на <b>каждую плитку</b> скрипт,
|
||
который убирает её через секунду после касания.
|
||
</p>
|
||
<ScriptKind kind="object" on="каждую плитку" />
|
||
<Code>{`// === Скрипт исчезающей плитки ===
|
||
|
||
let triggered = false; // плитка уже запущена на исчезновение?
|
||
|
||
game.self.onTouch(() => {
|
||
if (triggered) return; // если уже запущена — выходим
|
||
triggered = true;
|
||
game.sound.play('click');
|
||
// через 1.2 секунды плитка пропадает
|
||
game.after(1.2, () => {
|
||
game.self.delete();
|
||
});
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>triggered</code> — флажок-защёлка. Игрок может
|
||
коснуться плитки несколько раз, но запустить таймер
|
||
нужно только один раз — флажок это гарантирует;</li>
|
||
<li><code>game.after(1.2, () => {'{...}'})</code> —
|
||
«через 1.2 секунды выполни это». Внутри —
|
||
<code> game.self.delete()</code>, плитка исчезает;</li>
|
||
<li>можно поменять <code>1.2</code> на другое число —
|
||
меньше значит сложнее, плитки пропадают быстрее.</li>
|
||
</ul>
|
||
<Note>
|
||
Звук <code>game.sound.play('click')</code> при касании —
|
||
подсказка игроку: «плитка пошла исчезать, беги!».
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>встаёшь на плитку — щелчок, через секунду она пропадает;</li>
|
||
<li>стоишь на месте — проваливаешься, респаун на старте;</li>
|
||
<li>добежал до зелёного финиша — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай разные плитки с разной задержкой — одни исчезают
|
||
за 0.6с (быстрые), другие за 2с. Добавь на дорожку монетки
|
||
из урока 1 — собирай их на бегу.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 4 — «Кнопка-открывашка»
|
||
// ════════════════════════════════════════════════════
|
||
'button-door': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Комната перегорожена стеной с дверью. Игрок подходит
|
||
к красной кнопке, нажимает клавишу <kbd className="kbd">E</kbd> —
|
||
и дверь плавно уезжает вверх, открывая проход к финишу.
|
||
</p>
|
||
<Shot src="lesson4-result.png"
|
||
caption="Нажми E у кнопки — дверь откроется" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Взаимодействие по E (ProximityPrompt)</b> — как
|
||
сделать объект, с которым игрок «общается» клавишей E
|
||
(<code>game.self.onInteract</code>);</li>
|
||
<li><b>Поиск объекта по имени</b> — <code>game.scene.findOne</code>;</li>
|
||
<li><b>Твин</b> — как плавно подвинуть объект
|
||
(<code>game.tween</code>).</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Комната, стена и дверь</h3>
|
||
<Step n="1">
|
||
Сделай пол комнаты из блоков камня (примерно 16×24).
|
||
</Step>
|
||
<Step n="2">
|
||
Поперёк комнаты построй <b>стену</b> из двух кубов-примитивов,
|
||
оставив между ними проём шириной 3.
|
||
</Step>
|
||
<Step n="3">
|
||
В проём поставь <b>дверь</b> — куб 3×5×0.8, коричневый.
|
||
Дай ему имя <b>«Дверь»</b> — по имени его найдёт скрипт.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Кнопка и финиш</h3>
|
||
<Step n="1">
|
||
Перед дверью поставь <b>кнопку</b> — примитив-цилиндр,
|
||
низкий и широкий, красный неоновый. Имя — «Кнопка».
|
||
</Step>
|
||
<Step n="2">
|
||
За дверью поставь зелёную финишную площадку.
|
||
</Step>
|
||
<Shot src="lesson4-scene.png"
|
||
caption="Комната: кнопка, дверь в стене, финиш за ней" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «КНОПКА-ОТКРЫВАШКА» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт кнопки — главное</h3>
|
||
<p>
|
||
Скрипт кнопки делает всю работу: ловит нажатие E и
|
||
открывает дверь.
|
||
</p>
|
||
<ScriptKind kind="object" on="красную кнопку" />
|
||
<Code>{`// === Скрипт кнопки ===
|
||
|
||
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 // на сколько метров подойти
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.self.onInteract(fn, опции)</code> — это
|
||
и есть взаимодействие по E. Когда игрок подходит ближе
|
||
чем на <code>distance</code> метров, над кнопкой
|
||
появляется подсказка «Открыть дверь». Нажатие
|
||
<kbd className="kbd">E</kbd> запускает функцию;</li>
|
||
<li><code>game.scene.findOne('Дверь')</code> — находит дверь
|
||
по имени, которое мы задали в инспекторе;</li>
|
||
<li><code>game.tween(door, {'{ y: 8 }'}, ...)</code> —
|
||
плавно меняет высоту двери с текущей до 8, за 1.2
|
||
секунды. Дверь уезжает вверх — проход открыт.</li>
|
||
</ul>
|
||
<Note>
|
||
Почему твин, а не просто переставить дверь? Твин двигает
|
||
<b> плавно</b> — дверь красиво «уезжает». Если бы мы просто
|
||
задали новую высоту, дверь дёрнулась бы рывком.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`}</Code>
|
||
<p>Запусти игру:</p>
|
||
<ul>
|
||
<li>подойди к кнопке — появится подсказка «Открыть дверь»;</li>
|
||
<li>нажми <kbd className="kbd">E</kbd> — дверь плавно
|
||
уедет вверх;</li>
|
||
<li>пройди в открытый проём, встань на финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай две кнопки и две двери — каждая кнопка открывает
|
||
свою дверь. Или наоборот: чтобы дверь открылась, нужно
|
||
нажать <b>две</b> кнопки. Добавь звук закрытия и сделай,
|
||
чтобы дверь сама закрывалась через 5 секунд.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 5 — «Лабиринт»
|
||
// ════════════════════════════════════════════════════
|
||
'maze': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Лабиринт из стен. Игрок появляется в одном углу и должен
|
||
найти путь к зелёному выходу в другом конце. Стены
|
||
высокие — через них не перепрыгнуть, нужно искать проход.
|
||
</p>
|
||
<Shot src="lesson5-result.png"
|
||
caption="Лабиринт сверху — найди путь к выходу" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Постройка из блоков</b> — как строить стены
|
||
и коридоры;</li>
|
||
<li><b>Планирование уровня</b> — как нарисовать лабиринт
|
||
так, чтобы из него был выход;</li>
|
||
<li><b>Триггер-финиш</b> — как сделать невидимую зону
|
||
выхода.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Нарисуй лабиринт на бумаге</h3>
|
||
<p>
|
||
Прежде чем строить — нарисуй лабиринт на листе в клетку.
|
||
Клетка = проход или стена. Отметь, где старт, где выход.
|
||
Обязательно проверь карандашом, что от старта до выхода
|
||
<b> есть путь</b> — иначе игру не пройти.
|
||
</p>
|
||
<Note>
|
||
Это самый важный шаг. Строить в редакторе по готовой
|
||
схеме легко. Строить «на глазок» — запутаешься и забудешь
|
||
оставить проход.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 2. Пол</h3>
|
||
<Step n="1">
|
||
Инструментом <kbd className="kbd">Блок</kbd> выложи
|
||
квадратный пол из камня — по размеру твоего лабиринта.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 3. Стены</h3>
|
||
<Step n="1">
|
||
По своей схеме поставь стены. Стена — это столбик из блоков
|
||
высотой 3 (чтобы игрок не перепрыгнул).
|
||
</Step>
|
||
<Step n="2">
|
||
Сначала построй внешнюю границу — рамку по краю пола.
|
||
Потом — внутренние перегородки-коридоры по схеме.
|
||
</Step>
|
||
<Step n="3">
|
||
Не забудь оставить проходы! Время от времени запускай игру
|
||
и проверяй, что лабиринт реально проходится.
|
||
</Step>
|
||
<Shot src="lesson5-scene.png"
|
||
caption="Стены лабиринта в редакторе" />
|
||
|
||
<h3 className="lessonH">Шаг 4. Старт и финиш</h3>
|
||
<Step n="1">
|
||
Поставь точку спавна (вкладка <i>Игра → Ставить спавн</i>)
|
||
в начале лабиринта.
|
||
</Step>
|
||
<Step n="2">
|
||
В конце лабиринта положи на пол зелёную неоновую плитку —
|
||
финиш. Размер чуть меньше клетки, «Столкновение» выключи,
|
||
имя — «Финиш».
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипты</h3>
|
||
<p>Скрипты совсем простые — лабиринт держится на постройке.</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ЛАБИРИНТ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 6. Проверка</h3>
|
||
<ul>
|
||
<li>пройди лабиринт от старта до зелёного выхода;</li>
|
||
<li>дошёл до выхода — «Победа» и конфетти;</li>
|
||
<li>убедись, что нигде нет «дыр» в стенах и тупиков
|
||
без смысла.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
спрячь в тупиках лабиринта монетки из урока 1 — пусть
|
||
игрок собирает их по пути. Сделай лабиринт побольше.
|
||
Добавь второй этаж — лестницу из блоков наверх.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 6 — «Цветные плитки»
|
||
// ════════════════════════════════════════════════════
|
||
'color-tiles': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
На полу — сетка серых плиток. Когда игрок наступает
|
||
на плитку, она становится ярко-зелёной. Цель — обойти
|
||
и раскрасить все плитки.
|
||
</p>
|
||
<Shot src="lesson6-result.png"
|
||
caption="Раскрашенные и серые плитки" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Смена цвета объекта</b> — <code>game.scene.setColor</code>;</li>
|
||
<li><b>game.self.ref</b> — как скрипт указывает «адрес»
|
||
своего объекта;</li>
|
||
<li><b>Счётчик с целью</b> — раскрашено столько-то из всех.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Сетка плиток</h3>
|
||
<Step n="1">
|
||
Сделай пол из блоков травы.
|
||
</Step>
|
||
<Step n="2">
|
||
Плитка — примитив-куб 1.8×0.3×1.8, серого цвета.
|
||
«Столкновение» включено (на плитку наступают).
|
||
</Step>
|
||
<Step n="3">
|
||
Дублируй плитку и разложи сеткой 6×6 — ровными рядами
|
||
с небольшими промежутками. Дай каждой понятное имя.
|
||
</Step>
|
||
<Shot src="lesson6-scene.png"
|
||
caption="Сетка серых плиток на полу" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ЦВЕТНЫЕ ПЛИТКИ» — главный скрипт ===
|
||
|
||
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 });
|
||
}
|
||
});`}</Code>
|
||
<Note>
|
||
Замени число <code>36</code> на столько плиток, сколько
|
||
реально поставил. Если сетка 5×5 — будет 25.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт плитки</h3>
|
||
<ScriptKind kind="object" on="каждую плитку" />
|
||
<Code>{`// === Скрипт цветной плитки ===
|
||
|
||
let painted = false; // плитка уже раскрашена?
|
||
|
||
game.self.onTouch(() => {
|
||
if (painted) return;
|
||
painted = true;
|
||
// меняем цвет плитки на ярко-зелёный
|
||
game.scene.setColor(game.self.ref, '#33dd55');
|
||
game.broadcast('paint'); // сообщаем главному скрипту о покраске
|
||
});`}</Code>
|
||
<p>Главное здесь:</p>
|
||
<ul>
|
||
<li><code>game.scene.setColor(ref, цвет)</code> — меняет
|
||
цвет объекта прямо во время игры;</li>
|
||
<li><code>game.self.ref</code> — «адрес» этой плитки.
|
||
Любая команда <code>game.scene.*</code> работает
|
||
по ref, и через <code>game.self.ref</code> скрипт
|
||
передаёт адрес своего объекта;</li>
|
||
<li><code>game.broadcast('paint')</code> — шлёт сообщение
|
||
«paint». Главный скрипт ловит его через
|
||
<code> game.onMessage('paint', ...)</code> и прибавляет
|
||
счёт. Скрипты живут в разных «песочницах» — переменные
|
||
одного не видны другому, общаются только сообщениями;</li>
|
||
<li>флажок <code>painted</code> — чтобы плитка
|
||
засчиталась только один раз.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>наступаешь на серую плитку — она зеленеет, счёт растёт;</li>
|
||
<li>раскрасил все — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай так, чтобы плитки красились в <b>случайный</b> цвет —
|
||
через <code>game.random</code>. Или сделай «вредные»
|
||
красные плитки: наступил — счёт обнулился, начинай заново.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 7 — «Поймай падающее»
|
||
// ════════════════════════════════════════════════════
|
||
'catch-falling': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
С неба каждые полторы секунды падает жёлтый куб в случайном
|
||
месте. Игрок бегает по площадке и ловит кубы — касаешься
|
||
куба, он засчитывается и исчезает. Поймай 15 кубов.
|
||
</p>
|
||
<Shot src="lesson7-result.png"
|
||
caption="Кубы падают с неба — лови их" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Спавн объектов из скрипта</b> — создавать кубы
|
||
прямо во время игры (<code>game.scene.spawn</code>);</li>
|
||
<li><b>Таймер <code>game.every</code></b> — делать что-то
|
||
снова и снова с интервалом;</li>
|
||
<li><b>Случайные числа</b> — <code>game.random</code>;</li>
|
||
<li><b>Отслеживание объектов</b> — как помнить, какие
|
||
объекты «наши».</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Площадка</h3>
|
||
<Step n="1">
|
||
Сделай площадку 16×16 из травы с каменным бортиком —
|
||
чтобы было где ловить и некуда падать.
|
||
</Step>
|
||
<p>
|
||
Кубы создаются скриптом, заранее их ставить не нужно —
|
||
вся игра в одном скрипте.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПОЙМАЙ ПАДАЮЩЕЕ» — главный скрипт ===
|
||
|
||
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 });
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём по частям:</p>
|
||
<ul>
|
||
<li><code>game.every(1.5, fn)</code> — каждые 1.5 секунды
|
||
выполняет функцию. В ней мы создаём куб;</li>
|
||
<li><code>game.scene.spawn('primitive:cube', опции)</code> —
|
||
создаёт куб прямо во время игры. Высота
|
||
<code> y: 14</code> — куб появляется высоко, а
|
||
<code> anchored: false</code> — он падает по физике;</li>
|
||
<li><code>game.random(-6, 6)</code> — случайное число
|
||
от -6 до 6, так куб падает в случайном месте;</li>
|
||
<li><code>caught[ref] = true</code> — помечаем уже пойманный
|
||
куб, чтобы одно касание не засчиталось дважды;</li>
|
||
<li><code>deleteAfter(cube, 6)</code> — если куб не поймали
|
||
за 6 секунд, он сам исчезнет (уже упал на землю).</li>
|
||
</ul>
|
||
<Note>
|
||
Зачем нужен <code>caught</code>? Касание куба может прийти
|
||
несколько раз подряд. Пометка <code>caught[ref]</code>
|
||
гарантирует: каждый куб засчитываем ровно один раз.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Проверка</h3>
|
||
<ul>
|
||
<li>кубы сыплются с неба каждые 1.5 секунды;</li>
|
||
<li>добежал и коснулся куба — звон, +1 очко;</li>
|
||
<li>не успел — куб упал и пропал;</li>
|
||
<li>поймал 15 — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
ускорь падение кубов — спавни их чаще (поменяй 1.5
|
||
на 0.8). Сделай редкие красные кубы, которые дают +5
|
||
очков. Добавь таймер: успей поймать 15 кубов за минуту.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 8 — «Беги к финишу»
|
||
// ════════════════════════════════════════════════════
|
||
'run-to-finish': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Длинная беговая трасса. Как только начинается игра —
|
||
включается секундомер. Игрок бежит к зелёному финишу.
|
||
Добежал — секундомер останавливается и показывает
|
||
результат. Можно соревноваться: кто быстрее.
|
||
</p>
|
||
<Shot src="lesson8-result.png"
|
||
caption="Беговая трасса с секундомером" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Секундомер</b> — <code>game.ui.timer</code>;</li>
|
||
<li><b>dt в onTick</b> — как измерять прошедшее время;</li>
|
||
<li><b>Округление</b> — показать время красиво.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Трасса</h3>
|
||
<Step n="1">
|
||
Выложи из блоков длинную дорожку — например, 6 блоков
|
||
в ширину и 60 в длину.
|
||
</Step>
|
||
<Step n="2">
|
||
По бокам поставь невысокие бортики, чтобы игрок
|
||
не сбежал с трассы.
|
||
</Step>
|
||
<Step n="3">
|
||
В конце трассы положи зелёную неоновую финишную
|
||
плитку, имя «Финиш».
|
||
</Step>
|
||
<Step n="4">
|
||
Точку спавна поставь в самом начале трассы.
|
||
</Step>
|
||
<Shot src="lesson8-scene.png"
|
||
caption="Трасса с финишем в конце" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «БЕГИ К ФИНИШУ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Главное здесь — измерение времени:</p>
|
||
<ul>
|
||
<li><code>game.onTick((dt) => {'{...}'})</code> —
|
||
функция получает <b>dt</b>: сколько секунд прошло
|
||
с прошлого кадра (обычно ~0.016 — это 1/60 секунды);</li>
|
||
<li><code>time = time + dt</code> — складывая все dt,
|
||
мы получаем, сколько всего прошло времени;</li>
|
||
<li><code>game.ui.timer = time</code> — выводит секундомер
|
||
на экран в формате ММ:СС;</li>
|
||
<li><code>Math.round(time * 10) / 10</code> — округляет
|
||
до одной цифры после запятой (12.3 вместо 12.34567).</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish'); // сообщаем главному скрипту о финише
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>с началом игры в углу побежал секундомер;</li>
|
||
<li>беги к финишу <kbd className="kbd">Shift</kbd> для
|
||
ускорения;</li>
|
||
<li>встал на зелёную плитку — секундомер замер,
|
||
показалось твоё время.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
поставь на трассе препятствия — кубы-стены, через которые
|
||
надо перепрыгивать. Сделай несколько «дорожек» и засекай
|
||
время на каждой. Покажи лучшее время через
|
||
<code> game.save</code> — рекорд.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 9 — «Светофор»
|
||
// ════════════════════════════════════════════════════
|
||
'traffic-light': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
В конце дорожки — большой шар-светофор. Он по очереди
|
||
становится зелёным и красным. На <b>зелёный</b> можно
|
||
бежать к финишу, на <b>красный</b> нужно замереть.
|
||
Двинулся на красный — игра возвращает на старт.
|
||
</p>
|
||
<Shot src="lesson9-result.png"
|
||
caption="Светофор: зелёный — беги, красный — замри" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Фазы по таймеру</b> — переключать состояние игры
|
||
через <code>game.after</code>;</li>
|
||
<li><b>Проверка движения</b> — как узнать, что игрок
|
||
сдвинулся (сравнить позиции);</li>
|
||
<li><b>game.scene.setColor</b> — менять цвет светофора.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Дорожка и светофор</h3>
|
||
<Step n="1">
|
||
Сделай длинную дорожку из блоков (8 в ширину, ~50 в длину).
|
||
</Step>
|
||
<Step n="2">
|
||
В конце дорожки, повыше, поставь большую <b>сферу</b> —
|
||
это светофор. Материал «Неон», цвет красный. Имя —
|
||
«Светофор».
|
||
</Step>
|
||
<Step n="3">
|
||
Перед светофором — зелёная финишная плитка. Точка спавна —
|
||
в начале дорожки.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>Это самый сложный скрипт пока — разберём внимательно.</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «СВЕТОФОР» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Как работают фазы:</p>
|
||
<ul>
|
||
<li><code>green()</code> ставит зелёный цвет и через
|
||
<code> game.after(3, red)</code> запланирует
|
||
переключение на красный через 3 секунды;</li>
|
||
<li><code>red()</code> — наоборот: красный, и через 2.5с
|
||
обратно <code>green</code>. Так свет мигает по кругу;</li>
|
||
<li>две функции вызывают друг друга через таймер — это
|
||
и есть «вечный» светофор.</li>
|
||
</ul>
|
||
<p>Как ловится движение:</p>
|
||
<ul>
|
||
<li>каждый кадр запоминаем позицию игрока в <code>prev</code>;</li>
|
||
<li><code>Math.hypot(dx, dz)</code> — на сколько игрок
|
||
сдвинулся за кадр;</li>
|
||
<li><code>moved / dt</code> — это его <b>скорость</b>.
|
||
Если на красный скорость больше 0.8 — игрок шевелится,
|
||
возвращаем на старт.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`}</Code>
|
||
<ul>
|
||
<li>светофор мигает зелёный/красный;</li>
|
||
<li>на зелёный беги, на красный замри;</li>
|
||
<li>двинулся на красный — вернёт на старт;</li>
|
||
<li>добежал до финиша на зелёный — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
делай красную фазу всё короче с каждым кругом — игра
|
||
будет напряжённее. Добавь второй светофор посередине.
|
||
Сделай несколько игроков-соперников.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 10 — «Прыжок-пружина»
|
||
// ════════════════════════════════════════════════════
|
||
'spring-jump': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Башня из площадок-этажей на разной высоте. Между ними —
|
||
оранжевые батуты. Игрок встаёт на батут, тот подбрасывает
|
||
его высоко вверх — на следующий этаж. Допрыгай до верха.
|
||
</p>
|
||
<Shot src="lesson10-result.png"
|
||
caption="Башня из батутов — прыгай всё выше" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>game.player.boostJump</b> — мощный подброс игрока
|
||
вверх;</li>
|
||
<li><b>Многоуровневая сцена</b> — строить вверх, не только
|
||
вширь;</li>
|
||
<li><b>Один скрипт на много объектов</b> — все батуты
|
||
работают одинаково.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Башня</h3>
|
||
<Step n="1">
|
||
Поставь стартовую площадку из блоков на земле.
|
||
</Step>
|
||
<Step n="2">
|
||
Сделай 2-3 площадки-этажа из кубов-примитивов на высотах
|
||
7, 14, 21 — друг над другом со сдвигом. «Закреплён»
|
||
и «Столкновение» включены.
|
||
</Step>
|
||
<Step n="3">
|
||
На вершине — зелёная финишная плитка.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Батуты</h3>
|
||
<Step n="1">
|
||
Батут — примитив-цилиндр, низкий и широкий (2×0.4×2),
|
||
оранжевый неоновый.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь по батуту на старте и на каждом этаже (кроме
|
||
верхнего) — там, откуда нужно прыгать выше.
|
||
</Step>
|
||
<Shot src="lesson10-scene.png"
|
||
caption="Этажи с батутами между ними" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПРЫЖОК-ПРУЖИНА» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт батута</h3>
|
||
<p>Повесь этот скрипт на <b>каждый</b> батут — он одинаковый.</p>
|
||
<ScriptKind kind="object" on="каждый батут" />
|
||
<Code>{`// === Скрипт батута ===
|
||
// Игрок встал на батут — мощный подброс вверх.
|
||
|
||
game.self.onTouch(() => {
|
||
game.player.boostJump(3.2); // 3.2 = в 3 раза выше обычного прыжка
|
||
game.sound.play('jump');
|
||
});`}</Code>
|
||
<p>
|
||
<code>game.player.boostJump(сила)</code> — мгновенно
|
||
подбрасывает игрока. <code>1</code> — как обычный прыжок,
|
||
<code> 3.2</code> — в три с лишним раза выше. Подбери число
|
||
так, чтобы игрок допрыгивал от батута до следующего этажа.
|
||
</p>
|
||
<Note>
|
||
Если игрок не долетает до этажа — увеличь силу
|
||
(<code>boostJump(4)</code>) или поставь этажи ближе.
|
||
Если перелетает — наоборот, уменьши.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`}</Code>
|
||
<ul>
|
||
<li>встал на батут — подлетел вверх;</li>
|
||
<li>попал на этаж — иди к следующему батуту;</li>
|
||
<li>добрался до зелёного верха — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай батуты с разной силой — где-то слабый, где-то очень
|
||
мощный. Добавь движущиеся этажи (твин из урока 2). Расставь
|
||
в воздухе монетки, которые ловишь во время полёта.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 11 — «Эхо-комната»
|
||
// ════════════════════════════════════════════════════
|
||
'echo-room': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Комната с шестью разноцветными плитками. Когда игрок
|
||
наступает на плитку — она звучит и вспыхивает искрами.
|
||
Каждая плитка — свой звук. Пройди все шесть, потом
|
||
встань на зелёный финиш в центре.
|
||
</p>
|
||
<Shot src="lesson11-result.png"
|
||
caption="Звуковые плитки эхо-комнаты" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Звук на событие</b> — <code>game.sound.play</code>;</li>
|
||
<li><b>game.self.position</b> — узнать, где находится
|
||
объект скрипта;</li>
|
||
<li><b>Частицы-вспышка</b> — эффект при касании.</li>
|
||
</ul>
|
||
<Note>
|
||
Звук в играх обязателен — игра без звука кажется «мёртвой».
|
||
Этот урок как раз про то, как оживить игру звуком.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 1. Комната</h3>
|
||
<Step n="1">
|
||
Сделай пол комнаты (14×14) и стены из блоков высотой 3 —
|
||
замкнутое пространство.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Звуковые плитки</h3>
|
||
<Step n="1">
|
||
Плитка — примитив-цилиндр, низкий и широкий, неоновый.
|
||
Сделай 6 штук <b>разных цветов</b> и расставь по комнате.
|
||
</Step>
|
||
<Step n="2">
|
||
В центре комнаты — зелёная финишная плитка.
|
||
</Step>
|
||
<Shot src="lesson11-scene.png"
|
||
caption="Шесть цветных плиток и финиш" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ЭХО-КОМНАТА» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт звуковой плитки</h3>
|
||
<p>
|
||
Повесь на каждую плитку. Поменяй звук — у каждой плитки
|
||
свой: <code>'coin'</code>, <code>'jump'</code>,
|
||
<code> 'pickup'</code>, <code>'click'</code>,
|
||
<code> 'hit'</code>.
|
||
</p>
|
||
<ScriptKind kind="object" on="каждую цветную плитку" />
|
||
<Code>{`// === Скрипт звуковой плитки ===
|
||
|
||
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'); // сообщаем главному скрипту о новой плитке
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.sound.play('coin')</code> — проигрывает
|
||
звук. Доступные звуки: <code>coin</code>, <code>jump</code>,
|
||
<code> pickup</code>, <code>click</code>, <code>hit</code>,
|
||
<code> win</code>, <code>lose</code>;</li>
|
||
<li><code>game.self.position</code> — координаты самой
|
||
плитки, над ней появятся искры;</li>
|
||
<li><code>game.broadcast('step')</code> — шлёт сообщение
|
||
главному скрипту, тот ловит его через
|
||
<code> game.onMessage('step', ...)</code>. Скрипты
|
||
в разных «песочницах» общаются только сообщениями;</li>
|
||
<li>звук играет при <b>каждом</b> касании (эхо!), а в счёт
|
||
плитка идёт только первый раз — за это отвечает
|
||
флажок <code>used</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish'); // сообщаем главному скрипту о финише
|
||
});`}</Code>
|
||
<ul>
|
||
<li>наступаешь на плитку — звук и искры;</li>
|
||
<li>прошёл все 6 — появится подсказка идти на финиш;</li>
|
||
<li>встал на финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай мелодию: пусть игрок наступает на плитки в нужном
|
||
порядке (это будет похоже на музыкальную игру). Добавь
|
||
больше плиток. Меняй цвет плитки после нажатия.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 12 — «Дверь по коду»
|
||
// ════════════════════════════════════════════════════
|
||
'code-door': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Перед запертой дверью — четыре кнопки с цифрами. Чтобы
|
||
дверь открылась, нужно нажать кнопки (клавишей
|
||
<kbd className="kbd">E</kbd>) в <b>правильном порядке</b> —
|
||
знать секретный код. Ошибся — код сбрасывается, начинай
|
||
заново.
|
||
</p>
|
||
<Shot src="lesson12-result.png"
|
||
caption="Кнопки-цифры — набери код, чтобы открыть дверь" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Хранить последовательность</b> — массив нажатых
|
||
кнопок;</li>
|
||
<li><b>Проверять порядок</b> — совпадает ли ввод с кодом;</li>
|
||
<li><b>Сбрасывать прогресс</b> — при ошибке начать заново.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Комната, дверь, кнопки</h3>
|
||
<Step n="1">
|
||
Сделай комнату со стеной и дверью — как в уроке 4. Дверь
|
||
назови «Дверь».
|
||
</Step>
|
||
<Step n="2">
|
||
Перед дверью поставь <b>4 кнопки-цилиндра</b> в ряд.
|
||
Назови их «Кнопка_1», «Кнопка_2», «Кнопка_3», «Кнопка_4».
|
||
</Step>
|
||
<Step n="3">
|
||
За дверью — зелёный финиш.
|
||
</Step>
|
||
<Shot src="lesson12-scene.png"
|
||
caption="Четыре кнопки-цифры перед дверью" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Здесь самое интересное — проверка кода. Разберём подробно.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ДВЕРЬ ПО КОДУ» — главный скрипт ===
|
||
|
||
// СЕКРЕТНЫЙ КОД — порядок кнопок. Поменяй на свой!
|
||
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>
|
||
<p>Как работает проверка кода:</p>
|
||
<ul>
|
||
<li><code>CODE = [3, 1, 4, 2]</code> — секретный код,
|
||
массив номеров кнопок по порядку;</li>
|
||
<li><code>entered</code> — список того, что игрок уже нажал;</li>
|
||
<li><code>game.onMessage('press', (d) => ...)</code> —
|
||
ловит сообщение от кнопки. Номер кнопки приходит
|
||
в <code>d.num</code>. Кнопки шлют его через
|
||
<code> game.broadcast('press', {'{ num: ... }'})</code> —
|
||
скрипты в разных «песочницах» общаются только
|
||
сообщениями;</li>
|
||
<li><code>entered.push(d.num)</code> — добавляем новое
|
||
нажатие в конец списка;</li>
|
||
<li><code>entered[i] !== CODE[i]</code> — проверяем: совпало
|
||
ли последнее нажатие с нужной цифрой кода. Не совпало —
|
||
<code> entered = []</code> очищает список, начинай
|
||
заново;</li>
|
||
<li>когда длина <code>entered</code> равна длине
|
||
<code> CODE</code> и всё совпало — код разгадан,
|
||
дверь уезжает твином.</li>
|
||
</ul>
|
||
<Note>
|
||
Хитрость: мы проверяем каждое нажатие <b>сразу</b>. Если
|
||
игрок ошибся на втором шаге — не нужно ждать четвёртого,
|
||
код сбросится тут же.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипты кнопок</h3>
|
||
<p>
|
||
На каждую кнопку — свой скрипт. Отличается только
|
||
цифра в <code>press(...)</code> и в подсказке.
|
||
</p>
|
||
<ScriptKind kind="object" on="Кнопку_1 (на остальных — поменяй цифру)" />
|
||
<Code>{`// === Скрипт кнопки-цифры 1 ===
|
||
|
||
game.self.onInteract(() => {
|
||
// сообщаем главному скрипту номер нажатой кнопки
|
||
game.broadcast('press', { num: 1 }); // ← номер кнопки
|
||
}, {
|
||
text: 'Нажать кнопку 1',
|
||
distance: 3
|
||
});`}</Code>
|
||
<p>
|
||
Для «Кнопки_2» поставь <code>{'{ num: 2 }'}</code> и текст
|
||
«Нажать кнопку 2», и так далее.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`}</Code>
|
||
<ul>
|
||
<li>подходи к кнопкам, жми <kbd className="kbd">E</kbd>;</li>
|
||
<li>нажал по коду 3-1-4-2 — дверь открылась;</li>
|
||
<li>ошибся — «Код сброшен», начинай сначала;</li>
|
||
<li>прошёл в дверь, встал на финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай код длиннее — 6 кнопок. Подскажи код игроку
|
||
через надписи-billboard над кнопками. Сделай так, чтобы
|
||
после трёх ошибок включалась «сигнализация» — звук.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 13 — «Торговец»
|
||
// ════════════════════════════════════════════════════
|
||
'trader': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
В лавке за прилавком стоит NPC-торговец. Игрок подходит,
|
||
нажимает <kbd className="kbd">E</kbd> — торговец отвечает
|
||
репликой и дарит ключ. С ключом можно открыть дверь
|
||
и дойти до финиша.
|
||
</p>
|
||
<Shot src="lesson13-result.png"
|
||
caption="Торговец за прилавком — поговори и получи ключ" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>NPC</b> — создавать персонажей-жителей
|
||
(<code>game.scene.spawnNpc</code>);</li>
|
||
<li><b>Реплики NPC</b> — <code>npc.say</code>;</li>
|
||
<li><b>Инвентарь</b> — выдавать и проверять предметы
|
||
(<code>game.inventory</code>).</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Лавка</h3>
|
||
<Step n="1">
|
||
Сделай пол лавки из деревянных блоков.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь <b>прилавок</b> — длинный куб-примитив. Имя
|
||
«Прилавок».
|
||
</Step>
|
||
<Step n="3">
|
||
Сделай стену с дверью (имя «Дверь») и зелёный финиш
|
||
за ней — как в уроке 4.
|
||
</Step>
|
||
<Shot src="lesson13-scene.png"
|
||
caption="Лавка: прилавок, дверь, финиш" />
|
||
<Note>
|
||
Самого торговца ставить не нужно — он появится из скрипта
|
||
командой <code>spawnNpc</code>.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ТОРГОВЕЦ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.scene.spawnNpc('character-a', опции)</code> —
|
||
создаёт NPC. <code>speed: 0</code> — торговец не ходит,
|
||
стоит за прилавком;</li>
|
||
<li><code>trader.say('текст', 4)</code> — над головой NPC
|
||
на 4 секунды появляется реплика;</li>
|
||
<li><code>game.inventory.add({'{ name: '}'Ключ'{' }'})</code> —
|
||
кладёт предмет в инвентарь игрока;</li>
|
||
<li><code>game.inventory.has('Ключ')</code> — проверяет,
|
||
есть ли предмет. Дверь открывается только с ключом.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт прилавка</h3>
|
||
<ScriptKind kind="object" on="прилавок" />
|
||
<Code>{`// === Скрипт прилавка ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('talk'); // сообщаем главному скрипту: говорим с торговцем
|
||
}, {
|
||
text: 'Поговорить с торговцем',
|
||
distance: 4
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт двери</h3>
|
||
<ScriptKind kind="object" on="дверь" />
|
||
<Code>{`// === Скрипт двери ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('openDoor'); // сообщаем главному скрипту: открыть дверь
|
||
}, {
|
||
text: 'Открыть дверь',
|
||
distance: 4
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`}</Code>
|
||
<ul>
|
||
<li>подойди к прилавку, нажми <kbd className="kbd">E</kbd> —
|
||
торговец заговорит и даст ключ;</li>
|
||
<li>подойди к двери, нажми <kbd className="kbd">E</kbd> —
|
||
с ключом она откроется;</li>
|
||
<li>встань на финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай, чтобы торговец продавал ключ за монетки (собери
|
||
монетки из урока 1, потом купи). Добавь второго NPC
|
||
с другим предметом. Пусть торговец ходит за прилавком
|
||
через <code>npc.moveTo</code>.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 14 — «Собери по тегам»
|
||
// ════════════════════════════════════════════════════
|
||
'collect-by-tag': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
На площадке вперемешку стоят жёлтые звёзды и синие кубы.
|
||
Собирать нужно только звёзды — кубы это обманки. Игра
|
||
помечает все звёзды специальной меткой-<b>тегом</b>,
|
||
а потом по тегу легко считает, сколько звёзд ещё осталось.
|
||
</p>
|
||
<Shot src="lesson14-result.png"
|
||
caption="Жёлтые звёзды собираем, синие кубы — обманки" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Теги</b> — это «ярлычки», которые можно навесить
|
||
на объекты, чтобы потом находить именно их
|
||
(<code>game.scene.tag</code>);</li>
|
||
<li><b>Снять тег</b> — <code>game.scene.untag</code>;</li>
|
||
<li><b>Найти всё по тегу</b> — <code>game.scene.getTagged</code>
|
||
возвращает список всех объектов с этим тегом;</li>
|
||
<li><b>Считать остаток</b> — узнавать, сколько целей ещё
|
||
не собрано.</li>
|
||
</ul>
|
||
<Note>
|
||
Тег — это как наклейка. Можешь наклеить на всё, что нужно
|
||
собрать, ярлычок «звезда», а потом одной командой спросить:
|
||
«дай мне все объекты с ярлычком звезда». Очень удобно, когда
|
||
объектов много.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 1. Площадка</h3>
|
||
<Step n="1">
|
||
Инструментом <kbd className="kbd">Блок</kbd> выложи
|
||
квадратную травяную площадку примерно 16×16.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Звёзды и кубы-обманки</h3>
|
||
<Step n="1">
|
||
Звезда — примитив-<b>конус</b> жёлтого цвета
|
||
<code> #ffd700</code>, материал «Неон». Поставь 7 штук
|
||
и дай им имена «Звезда_1», «Звезда_2» ... «Звезда_7».
|
||
</Step>
|
||
<Step n="2">
|
||
«Столкновение» у звёзд <b>выключи</b> — игрок проходит
|
||
сквозь звезду, а касание поймает скрипт.
|
||
</Step>
|
||
<Step n="3">
|
||
Куб-обманка — примитив-<b>куб</b> синего цвета
|
||
<code> #3b82f6</code>. Поставь 5 штук вперемешку
|
||
со звёздами. Имена для них не важны — собирать их не надо.
|
||
</Step>
|
||
<Shot src="lesson14-scene.png"
|
||
caption="Звёзды-конусы и синие кубы-обманки на площадке" />
|
||
<Note>
|
||
Очень важно, чтобы звёзды назывались именно
|
||
«Звезда_1» ... «Звезда_7» — главный скрипт ищет их
|
||
по этим именам, чтобы навесить тег.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт сначала «наклеивает» тег на все звёзды,
|
||
а потом считает, сколько звёзд осталось.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «СОБЕРИ ПО ТЕГАМ» — главный скрипт ===
|
||
|
||
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 });
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём построчно:</p>
|
||
<ul>
|
||
<li>цикл <code>for</code> внутри <code>game.after(0.2, ...)</code>
|
||
проходит по звёздам с 1 по 7, находит каждую по имени
|
||
через <code>findOne</code> (небольшая задержка нужна,
|
||
чтобы снимок сцены успел появиться);</li>
|
||
<li><code>game.scene.tag(star, 'звезда')</code> — навешивает
|
||
на звезду тег-ярлычок «звезда»;</li>
|
||
<li><code>game.scene.getTagged('звезда')</code> — возвращает
|
||
<b> список</b> всех объектов с тегом «звезда». А
|
||
<code> .length</code> — это сколько их в списке;</li>
|
||
<li><code>7 - left</code> — если осталось 5 звёзд, значит
|
||
собрано 2 (всего 7 минус 5). Это и показываем в счёте;</li>
|
||
<li><code>if (left === 0)</code> — звёзд с тегом не осталось,
|
||
все собраны — победа.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт звезды</h3>
|
||
<p>Этот скрипт повесь на <b>каждую</b> из 7 звёзд.</p>
|
||
<ScriptKind kind="object" on="каждую звезду" />
|
||
<Code>{`// === Скрипт звезды ===
|
||
game.self.onTouch(() => {
|
||
// снимаем тег и удаляем звезду
|
||
game.scene.untag(game.self.ref, 'звезда');
|
||
game.self.delete();
|
||
game.broadcast('collected'); // сообщаем главному скрипту о сборе
|
||
});`}</Code>
|
||
<p>Что происходит при касании:</p>
|
||
<ul>
|
||
<li><code>game.scene.untag(game.self.ref, 'звезда')</code> —
|
||
снимает тег «звезда» с этой звезды. Теперь
|
||
<code> getTagged</code> её больше не вернёт;</li>
|
||
<li><code>game.self.delete()</code> — звезда исчезает
|
||
со сцены;</li>
|
||
<li><code>game.broadcast('collected')</code> — шлёт
|
||
сообщение главному скрипту. Тот ловит его через
|
||
<code> game.onMessage('collected', ...)</code> и
|
||
пересчитывает остаток. Скрипты в разных «песочницах»
|
||
общаются только сообщениями.</li>
|
||
</ul>
|
||
<Note>
|
||
У синих кубов скрипта нет — поэтому коснуться их можно,
|
||
но ничего не произойдёт. Тег есть только у звёзд, кубы —
|
||
просто украшение-обманка.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 5. Проверка</h3>
|
||
<ul>
|
||
<li>собирай жёлтые звёзды — счёт растёт, звенит монетка;</li>
|
||
<li>синие кубы можно трогать — ничего не происходит;</li>
|
||
<li>собрал все 7 звёзд — «Победа» и конфетти.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь второй тег — например, «бонус» на пару особых
|
||
красных звёзд, которые дают больше очков. Сделай больше
|
||
кубов-обманок. Покрась звёзды в разные цвета, но всё равно
|
||
помечай тегом «звезда».
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 15 — «Тир»
|
||
// ════════════════════════════════════════════════════
|
||
'shooting-range': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Настоящий тир: на постаментах стоят красные мишени-шары.
|
||
Нужно кликать по ним мышкой — за каждое попадание мишень
|
||
взрывается искрами и даёт очко. Выбей все 8 мишеней.
|
||
</p>
|
||
<Shot src="lesson15-result.png"
|
||
caption="Красные мишени на постаментах — кликай по ним" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Клик по 3D-объекту</b> — как сделать так, чтобы
|
||
объект реагировал на щелчок мышью
|
||
(<code>game.self.onClick</code>);</li>
|
||
<li><b>Счётчик попаданий</b> — считать, сколько мишеней
|
||
выбито;</li>
|
||
<li><b>Эффект-взрыв</b> — частицы при попадании.</li>
|
||
</ul>
|
||
<Note>
|
||
<code>onTouch</code> из прошлых уроков срабатывал, когда
|
||
игрок <b>касается</b> объекта телом. А <code>onClick</code> —
|
||
когда игрок <b>щёлкает</b> по объекту мышкой, даже издалека.
|
||
Для тира это как раз то, что нужно.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 1. Площадка тира</h3>
|
||
<Step n="1">
|
||
Выложи прямоугольную площадку из каменных блоков — это
|
||
зал тира.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Постаменты и мишени</h3>
|
||
<Step n="1">
|
||
Постамент — примитив-<b>куб</b>, узкий и высокий
|
||
(размер 1×2×1), серого цвета. Поставь 8 постаментов
|
||
дальше от точки старта, чтобы было куда целиться.
|
||
</Step>
|
||
<Step n="2">
|
||
Мишень — примитив-<b>сфера</b> ярко-красного цвета
|
||
<code> #ff3030</code>, материал «Неон». Поставь по сфере
|
||
на каждый постамент, повыше. У мишеней «Столкновение»
|
||
выключи. Дай им имена «Мишень_2», «Мишень_4» и т.д.
|
||
</Step>
|
||
<Shot src="lesson15-scene.png"
|
||
caption="Восемь мишеней-сфер на постаментах" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ТИР» — главный скрипт ===
|
||
|
||
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 });
|
||
}
|
||
});`}</Code>
|
||
<p>Здесь всё знакомо:</p>
|
||
<ul>
|
||
<li><code>score</code> — счётчик попаданий;</li>
|
||
<li><code>TOTAL = 8</code> — всего мишеней;</li>
|
||
<li><code>game.onMessage('hit', ...)</code> — ловит
|
||
сообщение «hit» от мишеней. Каждая мишень шлёт его
|
||
через <code>game.broadcast('hit')</code>, когда по ней
|
||
попали. Внутри +1 к счёту и проверка: выбиты ли все 8.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт мишени</h3>
|
||
<p>
|
||
Главное в этом уроке — повесь скрипт на <b>каждую</b>
|
||
мишень-сферу.
|
||
</p>
|
||
<ScriptKind kind="object" on="каждую мишень" />
|
||
<Code>{`// === Скрипт мишени ===
|
||
// Клик по 3D-объекту = выстрел в него.
|
||
|
||
game.self.onClick(() => {
|
||
// взрыв искр на месте мишени
|
||
game.scene.spawnParticles('explosion', game.self.position,
|
||
{ count: 1, color: '#ff6633' });
|
||
game.self.delete(); // мишень сбита
|
||
game.broadcast('hit'); // сообщаем главному скрипту о попадании
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.self.onClick(() => {'{...}'})</code> —
|
||
срабатывает, когда игрок щёлкнул мышкой по этой мишени;</li>
|
||
<li><code>game.scene.spawnParticles('explosion', ...)</code> —
|
||
на месте мишени вспыхивает взрыв оранжевых искр;</li>
|
||
<li><code>game.self.position</code> — координаты самой
|
||
мишени, там и будет взрыв;</li>
|
||
<li><code>game.self.delete()</code> — мишень исчезает,
|
||
а <code>game.broadcast('hit')</code> шлёт сообщение
|
||
главному скрипту — тот добавляет очко. Скрипты в разных
|
||
«песочницах» общаются только сообщениями.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 5. Проверка</h3>
|
||
<ul>
|
||
<li>наведи прицел на красную мишень и щёлкни мышкой;</li>
|
||
<li>мишень взрывается искрами, счёт растёт;</li>
|
||
<li>выбил все 8 мишеней — «Победа» и конфетти.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай несколько мишеней маленькими — по ним сложнее
|
||
попасть. Добавь «вредную» чёрную мишень: попал по ней —
|
||
минус очко. Сделай, чтобы мишени двигались твином
|
||
из урока 4 — попасть будет труднее.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 16 — «Лава-пол»
|
||
// ════════════════════════════════════════════════════
|
||
'lava-floor': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Перед игроком — большое озеро раскалённой лавы. Идти
|
||
по лаве нельзя — она жжёт и отнимает здоровье. Нужно
|
||
перепрыгивать с одного каменного островка на другой,
|
||
пока не доберёшься до зелёного финиша на той стороне.
|
||
</p>
|
||
<Shot src="lesson16-result.png"
|
||
caption="Прыгай по островкам — в лаву падать нельзя" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Урон игроку</b> — как отнять здоровье
|
||
(<code>game.player.damage</code>);</li>
|
||
<li><b>Здоровье (HP)</b> — у игрока есть полоска жизни,
|
||
она кончилась — игрок воскресает на старте;</li>
|
||
<li><b>Невидимая зона-триггер</b> — объект, который не виден,
|
||
но ловит касание игрока.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Озеро лавы и старт</h3>
|
||
<Step n="1">
|
||
Инструментом <kbd className="kbd">Блок</kbd> выбери блок
|
||
<b> лавы</b> и выложи большое прямоугольное озеро.
|
||
</Step>
|
||
<Step n="2">
|
||
В начале озера поставь поверх лавы небольшую
|
||
<b> стартовую площадку</b> из каменных блоков — отсюда
|
||
игрок начинает, тут безопасно.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Островки и финиш</h3>
|
||
<Step n="1">
|
||
Островок — примитив-<b>куб</b>, плоский и широкий
|
||
(размер примерно 2.4×0.6×2.4), серого цвета. Подними
|
||
его чуть над лавой. «Столкновение» и «Закреплён» включены.
|
||
</Step>
|
||
<Step n="2">
|
||
Дублируй островок и расставь дорожкой через всё озеро —
|
||
так, чтобы с каждого можно было допрыгнуть до следующего.
|
||
</Step>
|
||
<Step n="3">
|
||
В конце поставь зелёную неоновую финишную площадку,
|
||
имя «Финиш».
|
||
</Step>
|
||
<Shot src="lesson16-scene.png"
|
||
caption="Каменные островки дорожкой через озеро лавы" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Зона лавы</h3>
|
||
<p>
|
||
Чтобы лава наносила урон, над озером нужен один большой
|
||
<b> невидимый</b> куб-зона. Он ловит касание игрока.
|
||
</p>
|
||
<Step n="1">
|
||
Поставь куб-примитив и растяни его так, чтобы он накрыл
|
||
всё озеро лавы. Тонкий по высоте.
|
||
</Step>
|
||
<Step n="2">
|
||
В инспекторе у этого куба <b>выключи «Видимый»</b> и
|
||
<b> выключи «Столкновение»</b>. Дай имя «ЛаваЗона».
|
||
</Step>
|
||
<Note>
|
||
Почему отдельная невидимая зона, а не сами блоки лавы?
|
||
На блоки скрипт повесить нельзя, а на примитив — можно.
|
||
Поэтому делаем один большой невидимый примитив-ловушку
|
||
поверх лавы.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ЛАВА-ПОЛ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<ul>
|
||
<li><code>game.onTick</code> следит, не провалился ли игрок
|
||
совсем низко — тогда возвращает его на старт;</li>
|
||
<li><code>game.onMessage('win', ...)</code> — победа.
|
||
Финиш шлёт сюда сообщение «win» через
|
||
<code> game.broadcast('win')</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипт лавы — главное</h3>
|
||
<ScriptKind kind="object" on="невидимую зону «ЛаваЗона»" />
|
||
<Code>{`// === Скрипт лавы ===
|
||
// Игрок коснулся лавы — урон. У damage есть защита (i-frames),
|
||
// так что урон не каждый кадр, а раз в ~0.5 секунды.
|
||
|
||
game.self.onTouch(() => {
|
||
game.player.damage(20);
|
||
game.sound.play('hit');
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.player.damage(20)</code> — отнимает у игрока
|
||
20 единиц здоровья. Когда HP дойдёт до нуля, игрок
|
||
сам воскреснет на старте;</li>
|
||
<li>урон срабатывает не каждый кадр, а примерно раз
|
||
в полсекунды — это встроенная «защита от частого урона»
|
||
(по-английски i-frames). Иначе на лаве здоровье
|
||
улетало бы мгновенно;</li>
|
||
<li>звук <code>'hit'</code> — сигнал «ой, жжётся!».</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 6. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win'); // сообщаем главному скрипту о победе
|
||
});`}</Code>
|
||
<ul>
|
||
<li>прыгай с островка на островок;</li>
|
||
<li>попал в лаву — здоровье тает, слышен звук урона;</li>
|
||
<li>здоровье кончилось — воскресаешь на старте;</li>
|
||
<li>добрался до зелёного финиша — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай урон лавы посильнее (<code>damage(35)</code>) —
|
||
будет сложнее. Добавь по пути «аптечку» — зелёную сферу,
|
||
касание которой лечит через <code>game.player.heal</code>.
|
||
Сделай острова поменьше — прыгать точнее.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 17 — «Ключ и сундук»
|
||
// ════════════════════════════════════════════════════
|
||
'key-chest': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
На поляне среди зелёных кустов спрятан золотой ключ.
|
||
Игрок ищет ключ, подбирает его, а потом подходит
|
||
к сундуку и открывает его клавишей <kbd className="kbd">E</kbd>.
|
||
Без ключа сундук не откроется.
|
||
</p>
|
||
<Shot src="lesson17-result.png"
|
||
caption="Найди ключ среди кустов и открой сундук" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Инвентарь</b> — у игрока есть «рюкзак», куда можно
|
||
класть предметы (<code>game.inventory.add</code>);</li>
|
||
<li><b>Проверка предмета</b> — есть ли нужная вещь
|
||
в инвентаре (<code>game.inventory.has</code>);</li>
|
||
<li><b>Взаимодействие по E</b> — открыть сундук
|
||
(<code>game.self.onInteract</code>).</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поляна и кусты</h3>
|
||
<Step n="1">
|
||
Выложи большую квадратную травяную площадку — это поляна.
|
||
</Step>
|
||
<Step n="2">
|
||
Куст — примитив-<b>куб</b> зелёного цвета
|
||
<code> #2f7d32</code>. Поставь 5-6 кустов вразброс
|
||
по поляне — среди них и спрячется ключ.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Ключ и сундук</h3>
|
||
<Step n="1">
|
||
Ключ — примитив-<b>тор</b> (колечко) жёлтого цвета
|
||
<code> #ffd700</code>, материал «Неон». Положи его
|
||
где-нибудь в уголке, рядом с кустом. «Столкновение»
|
||
у ключа выключи. Имя — «Ключ».
|
||
</Step>
|
||
<Step n="2">
|
||
Сундук — примитив-<b>куб</b> коричневого цвета. Поставь
|
||
его в центре поляны. Имя — «Сундук».
|
||
</Step>
|
||
<Shot src="lesson17-scene.png"
|
||
caption="Кусты, спрятанный ключ-колечко и сундук" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «КЛЮЧ И СУНДУК» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Здесь два обработчика сообщений:</p>
|
||
<ul>
|
||
<li><code>game.onMessage('takeKey', ...)</code> — игрок
|
||
нашёл ключ. <code>game.inventory.add({'{ name: '}'Ключ'{' }'})</code>
|
||
кладёт предмет «Ключ» в рюкзак игрока;</li>
|
||
<li><code>game.onMessage('openChest', ...)</code> — попытка
|
||
открыть сундук. Сначала <code>game.inventory.has('Ключ')</code>
|
||
проверяет, есть ли ключ в рюкзаке. Если нет — сообщение
|
||
«Сундук заперт» и <code>return</code> прерывает функцию;</li>
|
||
<li>если ключ есть — победа, летит конфетти;</li>
|
||
<li>сообщения «takeKey» и «openChest» шлют скрипты ключа
|
||
и сундука через <code>game.broadcast(...)</code> —
|
||
скрипты в разных «песочницах» общаются только
|
||
сообщениями.</li>
|
||
</ul>
|
||
<Note>
|
||
<code>return</code> — это «выйти из функции прямо сейчас».
|
||
Если ключа нет, дальше код просто не выполняется — сундук
|
||
остаётся закрытым.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт ключа</h3>
|
||
<ScriptKind kind="object" on="ключ" />
|
||
<Code>{`// === Скрипт ключа ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('takeKey'); // сообщаем главному скрипту: ключ найден
|
||
game.self.delete(); // ключ подобран
|
||
});`}</Code>
|
||
<p>
|
||
Игрок коснулся ключа — скрипт шлёт сообщение
|
||
<code> game.broadcast('takeKey')</code> (главный скрипт
|
||
положит ключ в инвентарь), а <code>game.self.delete()</code>
|
||
убирает ключ с поляны: он же теперь в рюкзаке.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипт сундука</h3>
|
||
<ScriptKind kind="object" on="сундук" />
|
||
<Code>{`// === Скрипт сундука ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('openChest'); // сообщаем главному скрипту: открыть сундук
|
||
}, { text: 'Открыть сундук', distance: 4 });`}</Code>
|
||
<p>
|
||
Когда игрок подходит ближе чем на 4 метра, над сундуком
|
||
появляется подсказка «Открыть сундук». Нажатие
|
||
<kbd className="kbd">E</kbd> шлёт сообщение «openChest» —
|
||
а главный скрипт уже проверит, есть ли ключ.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 6. Проверка</h3>
|
||
<ul>
|
||
<li>подойди к сундуку без ключа, нажми
|
||
<kbd className="kbd">E</kbd> — «Сундук заперт»;</li>
|
||
<li>найди ключ-колечко, коснись его — «Ты нашёл Ключ»;</li>
|
||
<li>вернись к сундуку, нажми <kbd className="kbd">E</kbd> —
|
||
«Победа», конфетти.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
спрячь <b>два</b> ключа, и пусть сундук открывается только
|
||
когда оба в инвентаре. Сделай ключ совсем маленьким — искать
|
||
труднее. Добавь второй сундук с другой наградой.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 18 — «Качели»
|
||
// ════════════════════════════════════════════════════
|
||
'swing': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Между двумя площадками висят большие качели. Они сами
|
||
раскачиваются туда-сюда. Игрок запрыгивает на качели
|
||
с возвышенности и, прокатившись на них, перебирается
|
||
на финишную площадку.
|
||
</p>
|
||
<Shot src="lesson18-result.png"
|
||
caption="Качели раскачиваются — прокатись на них" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Констрейнт-петля (шарнир)</b> — как закрепить объект
|
||
на оси, чтобы он мог поворачиваться вокруг неё
|
||
(<code>game.constraints.hinge</code>);</li>
|
||
<li><b>Управлять углом петли</b> — <code>hinge.setAngle</code>;</li>
|
||
<li><b>Повторяющийся таймер</b> — <code>game.every</code>
|
||
для раскачивания.</li>
|
||
</ul>
|
||
<Note>
|
||
Констрейнт — это «связь» или «крепление». Петля-шарнир
|
||
(по-английски hinge) — это как петля у двери: объект не падает
|
||
и не уезжает, а только поворачивается вокруг оси. Качели
|
||
как раз качаются вокруг своей оси.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 1. Площадки</h3>
|
||
<Step n="1">
|
||
Сделай <b>стартовую возвышенность</b> — небольшую башенку
|
||
из каменных блоков высотой 2-3 блока. С неё игрок запрыгнет
|
||
на качели.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Качели и финиш</h3>
|
||
<Step n="1">
|
||
Качели — примитив-<b>куб</b>, широкий и плоский
|
||
(размер примерно 4×0.5×3), коричневого цвета. Подними его
|
||
в воздух на уровень возвышенности. Имя — «Качели».
|
||
</Step>
|
||
<Step n="2">
|
||
По другую сторону от качелей поставь зелёную неоновую
|
||
финишную площадку. Имя — «Финиш».
|
||
</Step>
|
||
<Shot src="lesson18-scene.png"
|
||
caption="Возвышенность, качели и финиш по другую сторону" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт делает качели «настоящими»: вешает их
|
||
на петлю и раскачивает.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «КАЧЕЛИ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём по частям:</p>
|
||
<ul>
|
||
<li><code>game.after(0.2, ...)</code> — ждём 0.2 секунды
|
||
перед поиском качелей. Снимок сцены приходит чуть позже
|
||
старта, поэтому <code>findOne</code> сразу позвать
|
||
нельзя — качелей ещё «не видно»;</li>
|
||
<li><code>game.scene.findOne('Качели')</code> — находим
|
||
качели по имени;</li>
|
||
<li><code>game.constraints.hinge(swing, опции)</code> —
|
||
вешаем качели на петлю-шарнир. <code>pivotX</code> и
|
||
<code> pivotZ</code> — где ось вращения (0, 0 — точно
|
||
посередине качелей);</li>
|
||
<li><code>hinge.setAngle(dir)</code> — поворачивает качели
|
||
на заданный угол. -35 — наклон в одну сторону,
|
||
35 — в другую;</li>
|
||
<li><code>dir = -dir</code> — меняет знак: было -35, стало 35,
|
||
потом снова -35. Так качели качаются туда-сюда;</li>
|
||
<li><code>game.every(1.4, ...)</code> — каждые 1.4 секунды
|
||
меняем угол — вот и раскачивание.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
|
||
<p>
|
||
Финиш ловит касание игрока и шлёт сообщение
|
||
<code> 'win'</code> — главный скрипт его поймает и покажет
|
||
победу.
|
||
</p>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>качели сами качаются туда-сюда;</li>
|
||
<li>запрыгни на них с возвышенности, поймай момент;</li>
|
||
<li>перебрался на зелёную площадку — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай качели быстрее — поменяй 1.4 на 0.9 секунды.
|
||
Увеличь угол наклона до 50 — размах станет больше.
|
||
Поставь подряд <b>двое</b> качелей — перепрыгивай с одних
|
||
на другие.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 19 — «Лифт»
|
||
// ════════════════════════════════════════════════════
|
||
'elevator': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
В комнате два этажа — нижний и верхний. Между ними ездит
|
||
синяя платформа-лифт: сама поднимается наверх и
|
||
опускается вниз, без остановки. Игрок встаёт на лифт,
|
||
дожидается верха и сходит на финиш.
|
||
</p>
|
||
<Shot src="lesson19-result.png"
|
||
caption="Синий лифт сам ездит вверх-вниз" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Твин с повтором</b> — как заставить движение
|
||
повторяться много раз (<code>repeat</code>);</li>
|
||
<li><b>Yoyo</b> — как сделать, чтобы объект ехал туда,
|
||
а потом сам возвращался обратно;</li>
|
||
<li><b>Постоянное движение</b> — лифт ездит «вечно».</li>
|
||
</ul>
|
||
<Note>
|
||
Слово «yoyo» — от игрушки йо-йо, которая то опускается,
|
||
то поднимается. В твине <code>yoyo: true</code> означает:
|
||
доехал до конца — поезжай обратно. А <code>repeat</code> —
|
||
сколько раз так повторить.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 1. Два этажа</h3>
|
||
<Step n="1">
|
||
Выложи из каменных блоков <b>нижний этаж</b> — пол комнаты.
|
||
</Step>
|
||
<Step n="2">
|
||
Сбоку, высоко над полом (примерно на высоте 12 блоков),
|
||
выложи <b>верхний этаж</b> — площадку, до которой нужно
|
||
добраться.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Лифт и финиш</h3>
|
||
<Step n="1">
|
||
Лифт — примитив-<b>куб</b>, плоский и широкий
|
||
(размер примерно 3.5×0.5×3.5), синего цвета
|
||
<code> #3357ff</code>, материал «Неон». Поставь его
|
||
на нижнем этаже. «Столкновение» и «Закреплён» включены.
|
||
Имя — «Лифт».
|
||
</Step>
|
||
<Step n="2">
|
||
На верхнем этаже поставь зелёную неоновую финишную
|
||
площадку. Имя — «Финиш».
|
||
</Step>
|
||
<Shot src="lesson19-scene.png"
|
||
caption="Нижний этаж, лифт и верхний этаж с финишем" />
|
||
|
||
<h3 className="lessonH">Шаг 3. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ЛИФТ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём твин лифта:</p>
|
||
<ul>
|
||
<li><code>game.after(0.2, ...)</code> — ждём 0.2 секунды:
|
||
снимок сцены приходит чуть позже старта, поэтому
|
||
<code> findOne</code> сразу позвать нельзя;</li>
|
||
<li><code>game.scene.findOne('Лифт')</code> — находим лифт
|
||
по имени;</li>
|
||
<li><code>game.tween(lift, {'{ y: 12.3 }'}, опции)</code> —
|
||
плавно поднимает лифт на высоту 12.3;</li>
|
||
<li><code>duration: 3.5</code> — подъём занимает 3.5 секунды;</li>
|
||
<li><code>yoyo: true</code> — доехав до верха, лифт сам
|
||
поедет обратно вниз;</li>
|
||
<li><code>repeat: 999</code> — повторить этот путь 999 раз,
|
||
то есть практически бесконечно — лифт ездит всю игру;</li>
|
||
<li><code>easing: 'ease'</code> — лифт мягко разгоняется
|
||
и тормозит, а не дёргается.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
|
||
<p>
|
||
Финиш ловит касание игрока и шлёт сообщение
|
||
<code> 'win'</code>. Главный скрипт ловит его через
|
||
<code> game.onMessage('win', ...)</code> — так два скрипта
|
||
из разных «песочниц» общаются.
|
||
</p>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>лифт ездит вверх-вниз сам по себе;</li>
|
||
<li>встань на него внизу и дождись верха;</li>
|
||
<li>сойди на верхний этаж, встань на финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай лифт медленнее (<code>duration: 5</code>) или
|
||
быстрее. Добавь второй лифт, который ездит в другую
|
||
сторону. Поставь на верхнем этаже монетки из урока 1.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 20 — «Имена над врагами»
|
||
// ════════════════════════════════════════════════════
|
||
'enemy-names': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
На арене стоят три врага: Гоблин, Скелет и Орк. Над
|
||
головой каждого висит метка с его именем и здоровьем.
|
||
Игрок подходит к врагу, кликает по нему — наносит урон,
|
||
и метка показывает, сколько HP осталось. Победи всех троих.
|
||
</p>
|
||
<Shot src="lesson20-result.png"
|
||
caption="Враги с именами и HP над головой" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Метки-billboard</b> — надписи, которые висят
|
||
над объектом и всегда повёрнуты к игроку
|
||
(<code>game.scene.setLabel</code>);</li>
|
||
<li><b>Убрать метку</b> — <code>game.scene.clearLabel</code>;</li>
|
||
<li><b>NPC-враги</b> — создавать врагов и наносить им урон;</li>
|
||
<li><b>Обновление метки</b> — менять текст метки, когда
|
||
здоровье врага меняется.</li>
|
||
</ul>
|
||
<Note>
|
||
Billboard-метка — это надпись в 3D-мире, которая всегда
|
||
«смотрит лицом» на игрока, как бы он ни ходил вокруг.
|
||
Так в играх показывают имена и полоски HP над персонажами.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 1. Арена</h3>
|
||
<Step n="1">
|
||
Выложи большую квадратную площадку из каменных блоков —
|
||
это арена.
|
||
</Step>
|
||
<p>
|
||
Врагов ставить руками не нужно — они появятся из скрипта
|
||
командой <code>spawnNpc</code>, как торговец в уроке 13.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Вся игра в одном скрипте. Он создаёт врагов, вешает
|
||
над ними метки и обрабатывает удары.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ИМЕНА НАД ВРАГАМИ» — главный скрипт ===
|
||
|
||
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 });
|
||
}
|
||
});
|
||
});`}</Code>
|
||
<p>Разберём по частям. Сначала про создание врагов:</p>
|
||
<ul>
|
||
<li><code>enemyData</code> — список врагов: у каждого имя,
|
||
координаты и запас здоровья;</li>
|
||
<li><code>enemyData.forEach((d) => {'{...}'})</code> —
|
||
перебираем список и для каждого делаем одно и то же;</li>
|
||
<li><code>game.scene.spawnNpc('character-b', ...)</code> —
|
||
создаёт NPC-врага. <code>speed: 0</code> — враг стоит
|
||
на месте.</li>
|
||
</ul>
|
||
<p>Теперь про метку над врагом:</p>
|
||
<ul>
|
||
<li><code>game.scene.setLabel(npc.ref, текст, опции)</code> —
|
||
вешает над врагом надпись. Текст склеен из имени и HP:
|
||
получится «Гоблин HP: 60»;</li>
|
||
<li><code>color</code> — цвет надписи, <code>height</code> —
|
||
на какой высоте над врагом она висит;</li>
|
||
<li><code>updateLabel()</code> — функция, которая заново
|
||
пишет метку. Зовём её каждый раз, когда HP изменилось.</li>
|
||
</ul>
|
||
<p>Про удар и гибель:</p>
|
||
<ul>
|
||
<li><code>game.onClick</code> — игрок щёлкнул мышью.
|
||
<code> Math.hypot(...)</code> считает расстояние
|
||
до врага: бьём, только если ближе 4 метров;</li>
|
||
<li><code>npc.damage(30)</code> — наносим врагу 30 урона,
|
||
<code> updateLabel()</code> обновляет цифру HP в метке;</li>
|
||
<li><code>npc.onDeath(...)</code> — когда враг погиб,
|
||
<code> game.scene.clearLabel(npc.ref)</code> убирает
|
||
его метку, а счётчик <code>alive</code> уменьшается;</li>
|
||
<li>когда <code>alive</code> дошёл до нуля — все враги
|
||
повержены, победа.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Проверка</h3>
|
||
<ul>
|
||
<li>над каждым врагом видна метка с именем и HP;</li>
|
||
<li>подойди к врагу ближе и кликай — HP в метке падает;</li>
|
||
<li>HP дошло до нуля — враг гибнет, метка пропадает;</li>
|
||
<li>победил всех троих — «Победа» и конфетти.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь четвёртого врага в список <code>enemyData</code> —
|
||
просто допиши строчку. Сделай врагов посильнее (больше HP).
|
||
Поменяй цвет метки в зависимости от здоровья: зелёная,
|
||
пока HP высокое, красная — когда мало.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 21 — «Преследователь»
|
||
// ════════════════════════════════════════════════════
|
||
'chaser': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
По полю за тобой гонится злой NPC-охотник. Он бегает
|
||
чуть быстрее обычного и не отстаёт. Нужно петлять между
|
||
каменными кубами-укрытиями и добежать до зелёного
|
||
укрытия-финиша. Поймал — игра возвращает на старт.
|
||
</p>
|
||
<Shot src="lesson21-result.png"
|
||
caption="Охотник гонится — добеги до зелёного укрытия" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>NPC-персонаж</b> — как создать живого героя
|
||
скриптом (<code>game.scene.spawnNpc</code>);</li>
|
||
<li><b>Преследование</b> — как заставить NPC гнаться
|
||
за игроком (<code>enemy.follow('player')</code>);</li>
|
||
<li><b>Расстояние между точками</b> — как узнать, далеко
|
||
ли враг (<code>Math.hypot</code>).</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле и укрытия</h3>
|
||
<Step n="1">
|
||
Инструментом <kbd className="kbd">Блок</kbd> выложи
|
||
большое травяное поле — широкое и длинное, чтобы было
|
||
где убегать.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь несколько больших серых кубов-примитивов
|
||
(3×3×3) — это укрытия, за которыми ты будешь петлять,
|
||
пока враг их обегает. «Столкновение» и «Закреплён»
|
||
включены.
|
||
</Step>
|
||
<Step n="3">
|
||
В дальнем конце поля положи зелёную неоновую площадку —
|
||
финиш. Имя — «Финиш». Точку спавна поставь в начале.
|
||
</Step>
|
||
<Shot src="lesson21-scene.png"
|
||
caption="Поле с серыми укрытиями и зелёным финишем" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт создаёт врага, запускает погоню и следит,
|
||
не догнал ли он игрока.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПРЕСЛЕДОВАТЕЛЬ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём построчно:</p>
|
||
<ul>
|
||
<li><code>game.scene.spawnNpc('character-b', опции)</code> —
|
||
создаёт NPC прямо во время игры. В опциях — где
|
||
появится, имя, здоровье и <code>speed</code> —
|
||
скорость бега;</li>
|
||
<li><code>enemy.follow('player')</code> — командует NPC
|
||
«гонись за игроком». Дальше враг сам бежит за тобой
|
||
каждый кадр;</li>
|
||
<li><code>Math.hypot(p.x - e.x, p.z - e.z)</code> —
|
||
расстояние между игроком и врагом. Если оно меньше
|
||
1.6 — враг вплотную, значит поймал;</li>
|
||
<li><code>enemy.stop()</code> в победе — враг замирает,
|
||
погоня закончена.</li>
|
||
</ul>
|
||
<Note>
|
||
Скорость врага <code>speed: 4</code> чуть больше скорости
|
||
обычной ходьбы. Если убегать слишком легко — увеличь её,
|
||
слишком сложно — уменьши.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<p>
|
||
Когда игрок касается финиша, скрипт шлёт сообщение
|
||
<code> game.broadcast('win')</code>. Главный скрипт ловит
|
||
его через <code>game.onMessage('win', ...)</code> —
|
||
останавливает врага и показывает «Победа». Так два скрипта
|
||
из разных «песочниц» общаются сообщениями.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>с началом игры за тобой бежит охотник;</li>
|
||
<li>петляй между серыми кубами, чтобы враг отстал;</li>
|
||
<li>поймал — респаун на старте;</li>
|
||
<li>добежал до зелёного укрытия — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь второго врага — просто создай ещё один NPC
|
||
и тоже дай ему <code>follow('player')</code>. Сделай
|
||
укрытий побольше. Спрячь по полю монетки из урока 1 —
|
||
собирай их на бегу.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 22 — «Зона опасности»
|
||
// ════════════════════════════════════════════════════
|
||
'danger-zone': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Длинный коридор, а посередине — большая красная зона.
|
||
Пока игрок внутри неё, у него тает здоровье. Перед зоной
|
||
лежит зелёная аптечка — подбери её, чтобы пополнить HP,
|
||
и пробеги опасный участок до финиша.
|
||
</p>
|
||
<Shot src="lesson22-result.png"
|
||
caption="Красная зона снимает HP — пробеги её быстро" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Вход и выход из зоны</b> — события
|
||
<code> onTouch</code> и <code>onUntouch</code>;</li>
|
||
<li><b>Урон игроку</b> — <code>game.player.damage</code>;</li>
|
||
<li><b>Лечение</b> — <code>game.player.heal</code>;</li>
|
||
<li><b>Повторяющийся таймер</b> — <code>game.every</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Коридор, зона и аптечка</h3>
|
||
<Step n="1">
|
||
Выложи длинный пол-коридор из камня.
|
||
</Step>
|
||
<Step n="2">
|
||
Посередине поставь большой красный куб-примитив, материал
|
||
«Неон». «Столкновение» <b>выключи</b> — игрок проходит
|
||
сквозь него, а вход поймает скрипт. Имя — «ЗонаОпасности».
|
||
</Step>
|
||
<Step n="3">
|
||
Перед зоной сбоку положи маленькую зелёную сферу —
|
||
аптечку. «Столкновение» выключено, имя — «Аптечка».
|
||
</Step>
|
||
<Step n="4">
|
||
В конце коридора — зелёная неоновая площадка-финиш,
|
||
имя «Финиш».
|
||
</Step>
|
||
<Shot src="lesson22-scene.png"
|
||
caption="Коридор с красной зоной, аптечкой и финишем" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ЗОНА ОПАСНОСТИ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>inZone</code> — флажок «игрок внутри красной
|
||
зоны». Скрипт зоны включает и выключает его
|
||
сообщениями;</li>
|
||
<li><code>game.every(0.6, fn)</code> — каждые 0.6 секунды
|
||
выполняет функцию. Внутри: если игрок в зоне —
|
||
<code> game.player.damage(12)</code> снимает 12 HP;</li>
|
||
<li><code>game.onMessage('zone-enter', ...)</code> и
|
||
<code> game.onMessage('zone-leave', ...)</code> — ловят
|
||
сообщения от зоны: «игрок вошёл» и «вышел». Зона шлёт
|
||
их через <code>game.broadcast</code>, потому что её
|
||
скрипт работает в отдельной «песочнице» и не видит
|
||
переменную <code>inZone</code> напрямую;</li>
|
||
<li><code>game.onMessage('win', ...)</code> — ловит
|
||
сообщение от финиша.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт зоны опасности</h3>
|
||
<ScriptKind kind="object" on="красную зону" />
|
||
<Code>{`// === Скрипт зоны опасности ===
|
||
// onTouch — игрок вошёл, onUntouch — вышел.
|
||
|
||
game.self.onTouch(() => {
|
||
game.broadcast('zone-enter');
|
||
});
|
||
game.self.onUntouch(() => {
|
||
game.broadcast('zone-leave');
|
||
});`}</Code>
|
||
<p>
|
||
Здесь важна <b>пара событий</b>: <code>onTouch</code>
|
||
срабатывает, когда игрок входит в зону, а
|
||
<code> onUntouch</code> — когда выходит. Так зона точно
|
||
знает, внутри игрок или нет.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт аптечки</h3>
|
||
<ScriptKind kind="object" on="зелёную аптечку" />
|
||
<Code>{`// === Скрипт аптечки ===
|
||
game.self.onTouch(() => {
|
||
game.player.heal(60);
|
||
game.ui.showText('+60 HP', 1.5);
|
||
game.sound.play('pickup');
|
||
game.self.delete();
|
||
});`}</Code>
|
||
<p>
|
||
<code>game.player.heal(60)</code> — добавляет игроку
|
||
60 единиц здоровья. Аптечку взяли один раз —
|
||
<code> game.self.delete()</code> убирает её.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>подбери зелёную аптечку — +60 HP;</li>
|
||
<li>в красной зоне здоровье тает каждые 0.6 секунды;</li>
|
||
<li>выбежал из зоны — урон прекратился;</li>
|
||
<li>добежал до финиша живым — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай зону опаснее — снимай больше HP или чаще. Поставь
|
||
несколько аптечек по пути. Добавь вторую зону другого
|
||
цвета, которая, наоборот, лечит, пока ты в ней стоишь.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 23 — «Переключатели»
|
||
// ════════════════════════════════════════════════════
|
||
'switches': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Перед стеной с дверью стоят три красных рычага. Дёргать
|
||
их (клавишей <kbd className="kbd">E</kbd>) нужно в
|
||
правильном порядке. Угадал последовательность — дверь
|
||
уезжает вверх. Ошибся — все рычаги сбрасываются, начинай
|
||
заново.
|
||
</p>
|
||
<Shot src="lesson23-result.png"
|
||
caption="Три рычага — дёрни их в нужном порядке" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Проверка последовательности</b> — как сравнить,
|
||
что нажал игрок, с правильным порядком;</li>
|
||
<li><b>Взаимодействие по E</b> — <code>game.self.onInteract</code>;</li>
|
||
<li><b>Массив-список</b> — как копить нажатия и сбрасывать
|
||
их при ошибке.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Стена, дверь и рычаги</h3>
|
||
<Step n="1">
|
||
Сделай пол комнаты из камня.
|
||
</Step>
|
||
<Step n="2">
|
||
Поперёк комнаты построй стену из двух кубов с проёмом
|
||
в центре. В проём поставь куб-дверь, имя — «Дверь».
|
||
</Step>
|
||
<Step n="3">
|
||
Перед дверью поставь три рычага — высокие красные
|
||
неоновые цилиндры. Имена — «Рычаг_1», «Рычаг_2»,
|
||
«Рычаг_3».
|
||
</Step>
|
||
<Step n="4">
|
||
За дверью — зелёная неоновая площадка-финиш.
|
||
</Step>
|
||
<Shot src="lesson23-scene.png"
|
||
caption="Три рычага перед стеной с дверью" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт хранит правильный порядок и проверяет
|
||
каждое нажатие.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПЕРЕКЛЮЧАТЕЛИ» — главный скрипт ===
|
||
|
||
// правильный порядок рычагов
|
||
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 });
|
||
});`}</Code>
|
||
<p>Самое интересное — проверка порядка:</p>
|
||
<ul>
|
||
<li><code>ORDER = [2, 3, 1]</code> — секретный порядок:
|
||
сначала второй рычаг, потом третий, потом первый.
|
||
Поменяй числа на свой порядок;</li>
|
||
<li><code>pressed.push(n)</code> — добавляет нажатый рычаг
|
||
в список того, что игрок уже дёрнул;</li>
|
||
<li><code>if (pressed[i] !== ORDER[i])</code> — сравниваем
|
||
последнее нажатие с тем, что должно быть. Не совпало —
|
||
<code> pressed = []</code> очищает список, начинай
|
||
заново;</li>
|
||
<li><code>if (pressed.length === ORDER.length)</code> —
|
||
все три рычага дёрнуты правильно — дверь уезжает
|
||
вверх через <code>game.tween</code>.</li>
|
||
</ul>
|
||
<Note>
|
||
Идея «копим список и сверяем по шагам» — та же, что
|
||
в игре «Дверь по коду» (урок 12). Так проверяют любые
|
||
комбинации: коды, пароли, последовательности.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт рычага</h3>
|
||
<p>
|
||
На каждый рычаг свой скрипт — отличается только номер.
|
||
Вот скрипт первого рычага:
|
||
</p>
|
||
<ScriptKind kind="object" on="Рычаг_1" />
|
||
<Code>{`// === Скрипт рычага 1 ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('lever', { num: 1 });
|
||
}, { text: 'Дёрнуть рычаг 1', distance: 3 });`}</Code>
|
||
<p>
|
||
Рычаг шлёт сообщение <code>game.broadcast('lever', {'{ num: 1 }'})</code>:
|
||
имя сообщения — <code>'lever'</code>, а второй кусок —
|
||
«посылка» с номером рычага. Главный скрипт ловит её
|
||
через <code>game.onMessage('lever', ...)</code> и узнаёт
|
||
номер из <code>d.num</code>.
|
||
</p>
|
||
<p>
|
||
На «Рычаг_2» — такой же скрипт, но с
|
||
<code> num: 2</code> и текстом «Дёрнуть рычаг 2»,
|
||
на «Рычаг_3» — с <code>num: 3</code>. Номер сообщает
|
||
главному скрипту, какой именно рычаг дёрнули.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>подойди к рычагу — появится подсказка «Дёрнуть рычаг»;</li>
|
||
<li>дёргай <kbd className="kbd">E</kbd> в верном
|
||
порядке — дверь откроется;</li>
|
||
<li>ошибся — «Неверно», начинай заново;</li>
|
||
<li>прошёл в проём, встал на финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь четвёртый рычаг и удлини <code>ORDER</code>.
|
||
Сделай рычаги одного цвета, чтобы порядок было сложнее
|
||
угадать. Покрась рычаг в зелёный, когда его дёрнули
|
||
правильно — через <code>game.scene.setColor</code>.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 24 — «Падающий мост»
|
||
// ════════════════════════════════════════════════════
|
||
'falling-bridge': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Над пропастью протянут мост из деревянных досок. Стоит
|
||
игроку наступить на доску — через секунду она рушится.
|
||
Значит, бежать нужно без остановки. Замешкался —
|
||
проваливаешься в пропасть.
|
||
</p>
|
||
<Shot src="lesson24-result.png"
|
||
caption="Беги по мосту — доски рушатся под ногами" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Таймер <code>game.after</code></b> — сделать
|
||
что-то через секунду, а не сразу;</li>
|
||
<li><b>Удаление объекта</b> — как убрать доску со сцены;</li>
|
||
<li><b>Флажок-защёлка</b> — чтобы доска запускалась
|
||
на падение только один раз.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Старт, пропасть и мост</h3>
|
||
<Step n="1">
|
||
Поставь твёрдую стартовую площадку из камня.
|
||
</Step>
|
||
<Step n="2">
|
||
Дальше — пустота: пропасть, через которую нет пола.
|
||
</Step>
|
||
<Step n="3">
|
||
Над пропастью выложи мост из досок — кубов-примитивов
|
||
2.4×0.35×2 коричневого цвета, в один ряд. «Столкновение»
|
||
и «Закреплён» включены. Имена — «Доска_1», «Доска_2»
|
||
и так далее.
|
||
</Step>
|
||
<Step n="4">
|
||
На другом краю пропасти — твёрдая площадка с зелёным
|
||
неоновым финишем, имя «Финиш».
|
||
</Step>
|
||
<Shot src="lesson24-scene.png"
|
||
caption="Мост из досок над пропастью" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПАДАЮЩИЙ МОСТ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>
|
||
Главный скрипт <code>game.onTick</code> каждый кадр
|
||
следит за высотой игрока: упал ниже -3 — провалился
|
||
в пропасть, возвращаем на старт. А
|
||
<code> game.onMessage('win', ...)</code> ждёт сообщение
|
||
от финиша — финиш работает в своей «песочнице», поэтому
|
||
о победе он сообщает через <code>game.broadcast</code>.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт доски</h3>
|
||
<p>Этот скрипт вешается на <b>каждую</b> доску моста.</p>
|
||
<ScriptKind kind="object" on="каждую доску" />
|
||
<Code>{`// === Скрипт доски моста ===
|
||
let cracking = false;
|
||
game.self.onTouch(() => {
|
||
if (cracking) return;
|
||
cracking = true;
|
||
game.sound.play('click');
|
||
game.after(1, () => { game.self.delete(); });
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>cracking</code> — флажок-защёлка. Игрок может
|
||
наступить на доску несколько раз, но запустить таймер
|
||
падения нужно только один раз;</li>
|
||
<li><code>game.after(1, fn)</code> — «через 1 секунду
|
||
выполни это». Внутри —
|
||
<code> game.self.delete()</code>, доска исчезает;</li>
|
||
<li>звук <code>'click'</code> при касании — сигнал
|
||
игроку: «доска затрещала, беги!».</li>
|
||
</ul>
|
||
<Note>
|
||
Скрипт одинаковый для всех досок. Создай его на первой
|
||
доске, а когда дублируешь доску
|
||
(<kbd className="kbd">Ctrl</kbd>+<kbd className="kbd">D</kbd>) —
|
||
скрипт скопируется вместе с ней.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>встал на доску — щелчок, через секунду она рушится;</li>
|
||
<li>стоишь на месте — проваливаешься, респаун на старте;</li>
|
||
<li>перебежал мост, встал на финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай доски «быстрее» — пусть рушатся не через 1, а через
|
||
0.6 секунды. Сделай мост подлиннее или с поворотом. Поставь
|
||
на нём монетки из урока 1 — собирай на бегу.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 25 — «Камера-облёт»
|
||
// ════════════════════════════════════════════════════
|
||
'flyby-camera': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Когда игра начинается, камера сама красиво облетает
|
||
уровень по нескольким точкам — показывает игроку, что
|
||
его ждёт. Как только облёт закончился, управление
|
||
возвращается, и игрок идёт к финишу.
|
||
</p>
|
||
<Shot src="lesson25-result.png"
|
||
caption="Камера облетает уровень в начале игры" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Кат-сцена камеры</b> — как сделать кино-облёт
|
||
уровня (<code>game.camera.cutscene</code>);</li>
|
||
<li><b>Событие конца облёта</b> —
|
||
<code> game.onCutsceneDone</code>;</li>
|
||
<li><b>Массив точек</b> — как задать путь, по которому
|
||
полетит камера.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Уровень с декорациями</h3>
|
||
<Step n="1">
|
||
Выложи широкое травяное поле — дорогу от старта
|
||
к финишу.
|
||
</Step>
|
||
<Step n="2">
|
||
Вдоль дороги расставь несколько фиолетовых неоновых
|
||
столбов-цилиндров — чтобы во время облёта было на что
|
||
посмотреть.
|
||
</Step>
|
||
<Step n="3">
|
||
В конце дороги — зелёная неоновая площадка-финиш,
|
||
имя «Финиш». Точку спавна поставь в начале.
|
||
</Step>
|
||
<Shot src="lesson25-scene.png"
|
||
caption="Дорога с фиолетовыми столбами и финишем" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт запускает облёт камеры и ждёт, когда он
|
||
закончится.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «КАМЕРА-ОБЛЁТ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.camera.cutscene([точки], опции)</code> —
|
||
запускает кино-облёт. Камера летит по списку точек
|
||
одна за другой;</li>
|
||
<li>каждая точка — это <code>{'{ x, y, z }'}</code>,
|
||
место, откуда камера смотрит на уровень;</li>
|
||
<li><code>segDuration: 1.8</code> — сколько секунд камера
|
||
летит от одной точки до следующей;</li>
|
||
<li><code>game.onCutsceneDone(fn)</code> — функция внутри
|
||
выполнится, когда облёт закончится. Тут мы показываем
|
||
подсказку «Вперёд, к финишу!».</li>
|
||
</ul>
|
||
<Note>
|
||
Облёт нужен, чтобы игрок сразу увидел весь уровень —
|
||
где он, куда бежать, что его ждёт. Так делают во многих
|
||
больших играх перед началом миссии.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт финиша и проверка</h3>
|
||
<p>
|
||
Финиш ловит касание игрока и шлёт сообщение
|
||
<code> 'win'</code>. Главный скрипт ловит его через
|
||
<code> game.onMessage('win', ...)</code> — так скрипты
|
||
из разных «песочниц» общаются между собой.
|
||
</p>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>с началом игры камера облетает уровень;</li>
|
||
<li>облёт закончился — управление вернулось, появилась
|
||
подсказка;</li>
|
||
<li>дойди до зелёного финиша — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь камере больше точек — облёт станет длиннее и
|
||
интереснее. Замедли облёт, увеличив <code>segDuration</code>.
|
||
Поставь по пути монетки или препятствия, которые камера
|
||
покажет во время облёта.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 26 — «Магнит монет»
|
||
// ════════════════════════════════════════════════════
|
||
'coin-magnet': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
По полю разбросаны золотые монетки. Стоит игроку
|
||
подойти к монетке поближе — она сама подлетает к нему,
|
||
будто притянутая магнитом. Собери все 8 монеток.
|
||
</p>
|
||
<Shot src="lesson26-result.png"
|
||
caption="Монетки сами летят к игроку — как магнит" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Проверка расстояния каждый кадр</b> — как монетка
|
||
«видит», что игрок рядом (<code>game.onTick</code> +
|
||
<code> Math.hypot</code>);</li>
|
||
<li><b>Твин к позиции игрока</b> — как заставить объект
|
||
лететь к игроку (<code>game.tween</code>);</li>
|
||
<li><b>Флажки состояния</b> — чтобы монетка летела один
|
||
раз и засчиталась один раз.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле и монетки</h3>
|
||
<Step n="1">
|
||
Выложи большое травяное поле.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь 8 монеток — жёлтые сферы-примитивы 0.6×0.6×0.6,
|
||
материал «Неон». «Столкновение» <b>выключи</b> — касание
|
||
поймает скрипт. Имена — «Монетка_1» … «Монетка_8».
|
||
</Step>
|
||
<Step n="3">
|
||
Расставь монетки по углам и краям поля — чтобы за каждой
|
||
нужно было подойти.
|
||
</Step>
|
||
<Shot src="lesson26-scene.png"
|
||
caption="Восемь монеток разбросаны по полю" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «МАГНИТ МОНЕТ» — главный скрипт ===
|
||
|
||
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 });
|
||
}
|
||
});`}</Code>
|
||
<p>
|
||
Главный скрипт простой — он только считает собранные
|
||
монетки и проверяет победу. Каждая монетка работает
|
||
в своей «песочнице», поэтому о сборе она сообщает через
|
||
<code> game.broadcast('coin')</code>, а главный скрипт
|
||
ловит сообщение через <code>game.onMessage('coin', ...)</code>.
|
||
Вся «магнитная магия» — в скрипте самой монетки.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт магнитной монетки</h3>
|
||
<p>Этот скрипт вешается на <b>каждую</b> монетку.</p>
|
||
<ScriptKind kind="object" on="каждую монетку" />
|
||
<Code>{`// === Скрипт магнитной монетки ===
|
||
|
||
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' });
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём по частям:</p>
|
||
<ul>
|
||
<li>первые кадры <code>game.self.position</code> и
|
||
<code> game.player.position</code> могут быть ещё
|
||
не готовы — проверка <code>if (!c || !p) return</code>
|
||
просто пропускает такой кадр;</li>
|
||
<li>каждый кадр считаем <code>dist</code> — расстояние
|
||
от монетки до игрока через <code>Math.hypot</code>;</li>
|
||
<li>если <code>dist {'<'} 1.2</code> — игрок дотянулся
|
||
до монетки. Ставим <code>taken</code>, удаляем
|
||
монетку и засчитываем её;</li>
|
||
<li>если <code>dist {'<'} 6</code> и монетка ещё
|
||
не летит — включаем <code>flying</code> и запускаем
|
||
<code> game.tween</code> к позиции игрока. Монетка
|
||
плавно подлетает за 0.5 секунды;</li>
|
||
<li>флажки <code>flying</code> и <code>taken</code>
|
||
нужны, чтобы твин запустился один раз и монетка
|
||
засчиталась один раз.</li>
|
||
</ul>
|
||
<Note>
|
||
Твин летит к той точке, где игрок был в момент запуска.
|
||
Если игрок отбежит — монетка прилетит «в пустоту»,
|
||
но <code>dist {'<'} 6</code> запустит её снова.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>подходишь к монетке ближе 6 метров — она летит к тебе;</li>
|
||
<li>монетка коснулась — звон, +1 очко;</li>
|
||
<li>собрал все 8 — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
увеличь «радиус магнита» — поменяй <code>6</code>
|
||
на большее число, монетки будут лететь издалека. Сделай
|
||
редкую красную монетку, которая <b>убегает</b> от игрока,
|
||
а не притягивается.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 27 — «Двойной прыжок»
|
||
// ════════════════════════════════════════════════════
|
||
'double-jump': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Паркур, где платформы стоят так далеко друг от друга,
|
||
что обычным прыжком до них не добраться. Скрипт включает
|
||
игроку <b>двойной прыжок</b> — можно прыгнуть ещё раз
|
||
прямо в воздухе и допрыгнуть до следующей платформы.
|
||
</p>
|
||
<Shot src="lesson27-result.png"
|
||
caption="Платформы далеко — нужен двойной прыжок" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Двойной прыжок</b> — особая способность игрока
|
||
(<code>game.player.setDoubleJump</code>);</li>
|
||
<li><b>Дизайн паркура</b> — как ставить платформы под
|
||
двойной прыжок;</li>
|
||
<li><b>Падение и респаун</b> — возврат на старт при
|
||
падении.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Старт и платформы</h3>
|
||
<Step n="1">
|
||
Поставь небольшую стартовую площадку из травы.
|
||
</Step>
|
||
<Step n="2">
|
||
Расставь 5 платформ — кубов-примитивов 2.5×0.5×2.5
|
||
коричневого цвета. Делай между ними <b>большие
|
||
разрывы</b> — такие, чтобы обычным прыжком не долететь.
|
||
«Столкновение» и «Закреплён» включены.
|
||
</Step>
|
||
<Step n="3">
|
||
Подними каждую следующую платформу чуть выше — дорожка
|
||
идёт вверх.
|
||
</Step>
|
||
<Step n="4">
|
||
В конце — зелёная неоновая площадка-финиш, имя «Финиш».
|
||
</Step>
|
||
<Shot src="lesson27-scene.png"
|
||
caption="Платформы с большими разрывами" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ДВОЙНОЙ ПРЫЖОК» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Главное здесь:</p>
|
||
<ul>
|
||
<li><code>game.player.setDoubleJump(true)</code> — даёт
|
||
игроку способность прыгать ещё раз в воздухе.
|
||
Достаточно одной этой строчки;</li>
|
||
<li>теперь, нажав <kbd className="kbd">Space</kbd> второй
|
||
раз во время полёта, игрок подпрыгивает ещё выше;</li>
|
||
<li><code>game.onTick</code> следит за падением — упал
|
||
ниже -3, возвращаем на старт.</li>
|
||
</ul>
|
||
<Note>
|
||
Без двойного прыжка этот паркур пройти нельзя — платформы
|
||
специально стоят слишком далеко. Способность меняет
|
||
правила игры.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт финиша и проверка</h3>
|
||
<p>
|
||
Финиш ловит касание игрока и шлёт сообщение
|
||
<code> 'win'</code>. Главный скрипт ловит его через
|
||
<code> game.onMessage('win', ...)</code> — так два скрипта
|
||
из разных «песочниц» общаются.
|
||
</p>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>прыгай <kbd className="kbd">Space</kbd>, в воздухе
|
||
жми ещё раз — второй прыжок;</li>
|
||
<li>так допрыгивай с платформы на платформу;</li>
|
||
<li>упал вниз — респаун на старте;</li>
|
||
<li>встал на зелёный финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
раздвинь платформы ещё дальше — паркур станет сложнее.
|
||
Добавь движущуюся платформу через <code>game.tween</code>
|
||
с <code>yoyo: true</code>. Поставь монетки между
|
||
платформами — собирай их в прыжке.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 28 — «Призрачные стены»
|
||
// ════════════════════════════════════════════════════
|
||
'ghost-walls': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Коридор перегорожен четырьмя фиолетовыми стенами. Каждую
|
||
можно сделать <b>призрачной</b>: кликнул по стене — она
|
||
становится полупрозрачной и проходимой. Пройди сквозь
|
||
все стены к финишу.
|
||
</p>
|
||
<Shot src="lesson28-result.png"
|
||
caption="Кликни по стене — она станет призрачной" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Клик по объекту</b> — событие
|
||
<code> game.self.onClick</code>;</li>
|
||
<li><b>Проходимость</b> — как убрать у объекта
|
||
столкновение во время игры
|
||
(<code>game.physics.passThrough</code>);</li>
|
||
<li><b>Прозрачность</b> — <code>game.scene.setOpacity</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Коридор и стены</h3>
|
||
<Step n="1">
|
||
Выложи длинный пол-коридор из камня.
|
||
</Step>
|
||
<Step n="2">
|
||
Поперёк коридора поставь 4 широкие фиолетовые стены —
|
||
кубы-примитивы, материал «Неон». «Столкновение»
|
||
включено — пока в них упираются. Имена — «Стена_1» …
|
||
«Стена_4».
|
||
</Step>
|
||
<Step n="3">
|
||
В конце коридора — зелёная неоновая площадка-финиш,
|
||
имя «Финиш».
|
||
</Step>
|
||
<Shot src="lesson28-scene.png"
|
||
caption="Коридор с четырьмя фиолетовыми стенами" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПРИЗРАЧНЫЕ СТЕНЫ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>
|
||
Главный скрипт только обрабатывает победу — он ловит
|
||
сообщение <code>'win'</code> от финиша через
|
||
<code> game.onMessage</code>. Вся «магия» — в скриптах
|
||
стен.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт призрачной стены</h3>
|
||
<p>Этот скрипт вешается на <b>каждую</b> стену.</p>
|
||
<ScriptKind kind="object" on="каждую стену" />
|
||
<Code>{`// === Скрипт призрачной стены ===
|
||
|
||
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);
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.self.onClick(fn)</code> — функция внутри
|
||
срабатывает, когда игрок кликнул по этой стене мышкой;</li>
|
||
<li><code>game.physics.passThrough(ref, true)</code> —
|
||
убирает у стены столкновение: теперь сквозь неё можно
|
||
пройти;</li>
|
||
<li><code>game.scene.setOpacity(ref, 0.25)</code> —
|
||
делает стену почти прозрачной (0.25 — четверть
|
||
непрозрачности), чтобы было видно: она призрачная;</li>
|
||
<li><code>ghost</code> — флажок, чтобы клики после
|
||
первого ничего не делали.</li>
|
||
</ul>
|
||
<Note>
|
||
Сначала меняем физику (<code>passThrough</code>), потом
|
||
внешний вид (<code>setOpacity</code>) — игрок и видит,
|
||
и чувствует, что стена «расколдована».
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>кликни по фиолетовой стене — она побледнеет;</li>
|
||
<li>пройди сквозь призрачную стену;</li>
|
||
<li>так пройди все 4 стены;</li>
|
||
<li>встань на зелёный финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай среди стен «обманки» — стены, которые при клике
|
||
не исчезают, а наоборот мигают красным. Добавь стенам
|
||
задержку: после клика стена становится призрачной только
|
||
через секунду (<code>game.after</code>).
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 29 — «Магазин»
|
||
// ════════════════════════════════════════════════════
|
||
'shop': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Игрок собирает монетки, разбросанные по комнате. Потом
|
||
подходит к прилавку и покупает ключ за 5 монет. С ключом
|
||
в инвентаре он открывает запертую дверь и доходит
|
||
до финиша.
|
||
</p>
|
||
<Shot src="lesson29-result.png"
|
||
caption="Собери монеты, купи ключ, открой дверь" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Валюта-экономика</b> — копить монеты и тратить их;</li>
|
||
<li><b>Инвентарь</b> — класть предмет игроку
|
||
(<code>game.inventory.add</code>) и проверять, есть ли
|
||
он (<code>game.inventory.has</code>);</li>
|
||
<li><b>Покупка</b> — проверить, хватает ли денег.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Комната, прилавок, дверь</h3>
|
||
<Step n="1">
|
||
Сделай пол комнаты из досок дерева.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь 7 монеток — жёлтые сферы-примитивы, «Столкновение»
|
||
выключено. Имена — «Монетка_1» … «Монетка_7».
|
||
</Step>
|
||
<Step n="3">
|
||
Поставь прилавок — длинный куб, имя «Прилавок».
|
||
</Step>
|
||
<Step n="4">
|
||
Дальше — стена из двух кубов с проёмом, в проёме
|
||
куб-дверь (имя «Дверь»). За дверью — зелёный неоновый
|
||
финиш.
|
||
</Step>
|
||
<Shot src="lesson29-scene.png"
|
||
caption="Магазин: монетки, прилавок, дверь, финиш" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт — «мозг» магазина: считает монеты,
|
||
продаёт ключ, открывает дверь.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «МАГАЗИН» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>
|
||
Каждый объект магазина — монетка, прилавок, дверь, финиш —
|
||
работает в своей «песочнице» и не видит переменные
|
||
главного скрипта. Поэтому объекты шлют сообщения через
|
||
<code> game.broadcast('имя')</code>, а главный скрипт ловит
|
||
их через <code>game.onMessage('имя', ...)</code>. Разберём
|
||
четыре обработчика:
|
||
</p>
|
||
<ul>
|
||
<li><code>onMessage('coin', ...)</code> — монетка собрана:
|
||
<code> coins</code> растёт, число на экране тоже;</li>
|
||
<li><code>onMessage('buy', ...)</code> — покупка ключа.
|
||
Сначала проверяем <code>coins {'<'} PRICE</code> —
|
||
хватает ли денег. Хватает — отнимаем 5 монет и
|
||
<code> game.inventory.add</code> кладёт ключ
|
||
в инвентарь;</li>
|
||
<li><code>onMessage('open-door', ...)</code> —
|
||
<code> game.inventory.has('Ключ')</code> проверяет,
|
||
есть ли ключ. Есть — дверь уезжает вверх;</li>
|
||
<li><code>onMessage('win', ...)</code> — победа на финише.</li>
|
||
</ul>
|
||
<Note>
|
||
Инвентарь — это «карман» игрока. Положил предмет через
|
||
<code> add</code>, проверил через <code>has</code>. Так
|
||
делают ключи, оружие, зелья — что угодно.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт монетки</h3>
|
||
<ScriptKind kind="object" on="каждую монетку" />
|
||
<Code>{`// === Скрипт монетки ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('coin');
|
||
game.self.delete();
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипт прилавка и двери</h3>
|
||
<ScriptKind kind="object" on="прилавок" />
|
||
<Code>{`// === Скрипт прилавка ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('buy');
|
||
}, { text: 'Купить ключ (5 монет)', distance: 4 });`}</Code>
|
||
<ScriptKind kind="object" on="дверь" />
|
||
<Code>{`// === Скрипт двери ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('open-door');
|
||
}, { text: 'Открыть дверь', distance: 4 });`}</Code>
|
||
<p>
|
||
И прилавок, и дверь — это объекты с взаимодействием по
|
||
<kbd className="kbd">E</kbd>. Подошёл к прилавку и нажал
|
||
E — объект шлёт сообщение <code>'buy'</code>, подошёл
|
||
к двери и нажал E — летит сообщение <code>'open-door'</code>.
|
||
Главный скрипт ловит эти сообщения и решает, что делать.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 5. Скрипт финиша и проверка</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('win');
|
||
});`}</Code>
|
||
<ul>
|
||
<li>собери монетки — счётчик растёт;</li>
|
||
<li>у прилавка нажми <kbd className="kbd">E</kbd> — если
|
||
монет 5 и больше, купишь ключ;</li>
|
||
<li>у двери нажми <kbd className="kbd">E</kbd> — с ключом
|
||
она откроется;</li>
|
||
<li>дойди до финиша — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь второй товар — например, «Зелье скорости» за 3
|
||
монеты. Сделай монеток побольше или подними цену ключа.
|
||
Поставь два прилавка с разными ценами.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 30 — «Квест с заданиями»
|
||
// ════════════════════════════════════════════════════
|
||
'quest-tasks': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
На поле стоит NPC-старейшина. Он даёт цепочку из трёх
|
||
заданий: собери монетку, дойди до синего флага, вернись
|
||
к нему. Каждое задание открывает следующее. Выполнил всё —
|
||
победа.
|
||
</p>
|
||
<Shot src="lesson30-result.png"
|
||
caption="Старейшина даёт цепочку заданий" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>NPC-диалоги</b> — как NPC «говорит»
|
||
(<code>npc.say</code>);</li>
|
||
<li><b>Этапы квеста</b> — как хранить, на каком шаге
|
||
квеста сейчас игрок (переменная-состояние);</li>
|
||
<li><b>Цепочка заданий</b> — как одно задание открывает
|
||
следующее.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле, тумба, монетка, флаг</h3>
|
||
<Step n="1">
|
||
Выложи большое травяное поле.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь тумбу-квестодателя — куб, имя «Квестодатель».
|
||
Рядом с ней скрипт создаст NPC.
|
||
</Step>
|
||
<Step n="3">
|
||
В одном углу поля поставь жёлтую сферу-монетку
|
||
(имя «КвестМонетка»), в другом — синий конус-флаг
|
||
(имя «КвестФлаг»). У обоих «Столкновение» выключено.
|
||
</Step>
|
||
<Shot src="lesson30-scene.png"
|
||
caption="Поле с квестодателем, монеткой и флагом" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт хранит этап квеста и ведёт игрока
|
||
по заданиям.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт ===
|
||
|
||
// этап квеста: 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);
|
||
});`}</Code>
|
||
<p>Главное здесь — переменная <code>stage</code>:</p>
|
||
<ul>
|
||
<li><code>stage</code> хранит, на каком шаге квеста
|
||
игрок: 0 — не начат, 1 — собирает монетку, 2 — идёт
|
||
к флагу, 3 — возвращается, 4 — готово;</li>
|
||
<li>квестодатель, монетка и флаг — отдельные скрипты,
|
||
каждый в своей «песочнице». Они не видят переменную
|
||
<code> stage</code>, поэтому шлют сообщения через
|
||
<code> game.broadcast('имя')</code>, а главный скрипт
|
||
ловит их через <code>game.onMessage('имя', ...)</code>;</li>
|
||
<li><code>onMessage('talk', ...)</code> — разговор с NPC.
|
||
Что он скажет, зависит от <code>stage</code>: на старте
|
||
даёт первое задание, в конце поздравляет;</li>
|
||
<li><code>onMessage('coin-done', ...)</code> и
|
||
<code> onMessage('flag-done', ...)</code> — ловят
|
||
сообщения от монетки и флага. Они проверяют
|
||
<code> stage</code> и переводят квест на следующий
|
||
этап;</li>
|
||
<li><code>npc.say('текст', 4)</code> — NPC показывает
|
||
над собой облачко с репликой на 4 секунды.</li>
|
||
</ul>
|
||
<Note>
|
||
Проверки вроде <code>if (stage !== 1) return</code> важны:
|
||
они не дают засчитать задание раньше времени. Монетку
|
||
нельзя «сдать», пока NPC не дал задание её собрать.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт квестодателя</h3>
|
||
<ScriptKind kind="object" on="тумбу квестодателя" />
|
||
<Code>{`// === Скрипт квестодателя ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('talk');
|
||
}, { text: 'Поговорить', distance: 4 });`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 4. Скрипты монетки и флага</h3>
|
||
<ScriptKind kind="object" on="квест-монетку" />
|
||
<Code>{`// === Скрипт квест-монетки ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('coin-done');
|
||
game.self.delete();
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="квест-флаг" />
|
||
<Code>{`// === Скрипт квест-флага ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('flag-done');
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 5. Проверка</h3>
|
||
<ul>
|
||
<li>подойди к квестодателю, нажми
|
||
<kbd className="kbd">E</kbd> — он даст первое
|
||
задание;</li>
|
||
<li>собери монетку — NPC даст второе задание;</li>
|
||
<li>дойди до флага — NPC попросит вернуться;</li>
|
||
<li>вернись к квестодателю и поговори — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь четвёртый этап квеста — например, «принеси два
|
||
предмета». Сделай NPC награду: после квеста дай игроку
|
||
предмет через <code>game.inventory.add</code>. Поставь
|
||
второго NPC со своим квестом.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 31 — «Защита базы»
|
||
// ════════════════════════════════════════════════════
|
||
'base-defense': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Из дальнего конца поля к твоей базе идут волны
|
||
NPC-врагов. Кликай по ним, чтобы уничтожать. Уничтожь 12
|
||
врагов — победа. Пропустишь 5 врагов до базы — поражение.
|
||
</p>
|
||
<Shot src="lesson31-result.png"
|
||
caption="Враги идут к базе — кликай по ним" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Волны врагов</b> — спавнить NPC снова и снова
|
||
(<code>game.every</code> + <code>spawnNpc</code>);</li>
|
||
<li><b>Движение NPC к точке</b> — <code>enemy.moveTo</code>;</li>
|
||
<li><b>Клик по врагу</b> — уничтожение
|
||
(<code>enemy.remove</code>);</li>
|
||
<li><b>Два счётчика</b> — победа и поражение
|
||
одновременно.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле и база</h3>
|
||
<Step n="1">
|
||
Выложи длинное поле из камня — дорогу, по которой пойдут
|
||
враги.
|
||
</Step>
|
||
<Step n="2">
|
||
У ближнего края поставь большой синий неоновый куб —
|
||
базу, имя «База». Точку спавна поставь рядом с базой.
|
||
</Step>
|
||
<p>
|
||
Враги создаются скриптом — заранее их ставить не нужно.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>Это большой скрипт — разберём его по частям.</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ЗАЩИТА БАЗЫ» — главный скрипт ===
|
||
|
||
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);
|
||
}
|
||
}
|
||
});
|
||
});`}</Code>
|
||
<p>Как появляются волны врагов:</p>
|
||
<ul>
|
||
<li><code>game.every(2, ...)</code> — каждые 2 секунды
|
||
создаётся новый враг в случайной точке у дальнего
|
||
края (<code>z: 38</code>);</li>
|
||
<li><code>enemy.moveTo(0, 2)</code> — командует врагу
|
||
идти к точке базы (координата 0) со скоростью 2.</li>
|
||
</ul>
|
||
<p>Что происходит с каждым врагом:</p>
|
||
<ul>
|
||
<li><code>game.onClick(...)</code> — клик мышкой. Если
|
||
игрок близко к врагу (меньше 5 метров) —
|
||
<code> enemy.remove()</code> убирает врага, летит
|
||
взрыв, <code>killed</code> растёт;</li>
|
||
<li><code>game.every(0.4, ...)</code> в переменной
|
||
<code> watch</code> — следит за врагом. Если он дошёл
|
||
до базы (<code>z {'<'} 4</code>) — счётчик
|
||
<code> leaked</code> растёт;</li>
|
||
<li><code>game.cancel(watch)</code> — останавливает этот
|
||
таймер-следилку, когда враг уже мёртв или ушёл;</li>
|
||
<li>две проверки: <code>killed {'>'}= GOAL</code> —
|
||
победа, <code>leaked {'>'}= MAX_LEAK</code> —
|
||
поражение.</li>
|
||
</ul>
|
||
<Note>
|
||
У каждого врага свой <code>game.onClick</code> и свой
|
||
таймер <code>watch</code>. Флажок <code>dead</code> —
|
||
личный для каждого врага: он не даёт убить или засчитать
|
||
одного врага дважды.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Проверка</h3>
|
||
<ul>
|
||
<li>каждые 2 секунды появляется новый враг;</li>
|
||
<li>подбеги и кликни по врагу — взрыв, +1 к счёту;</li>
|
||
<li>враг дошёл до базы — счётчик прорывов растёт;</li>
|
||
<li>12 уничтожено — «Победа», 5 прорвались — «Поражение».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
ускорь волны — спавни врагов чаще (поменяй 2 на 1).
|
||
Сделай врагов быстрее или живучее. Добавь «жирного»
|
||
врага раз в 5 волн — большого и медленного.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 32 — «Гонка с кругами»
|
||
// ════════════════════════════════════════════════════
|
||
'lap-race': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Кольцевая трасса с четырьмя чекпоинтами по углам. Нужно
|
||
проехать 2 круга, пересекая чекпоинты строго по порядку.
|
||
Сверху идёт секундомер — старайся уложиться побыстрее.
|
||
</p>
|
||
<Shot src="lesson32-result.png"
|
||
caption="Кольцевая трасса с чекпоинтами — 2 круга" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Чекпоинты по порядку</b> — как проверить, что
|
||
игрок проезжает точки в нужной последовательности;</li>
|
||
<li><b>Счётчик кругов</b> — как считать пройденные круги;</li>
|
||
<li><b>Секундомер</b> — <code>game.ui.timer</code> +
|
||
<code> dt</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Кольцевая трасса</h3>
|
||
<Step n="1">
|
||
Выложи квадратное кольцо из камня — большой квадрат
|
||
с дыркой в центре. По нему игрок будет наматывать круги.
|
||
</Step>
|
||
<Step n="2">
|
||
По четырём сторонам кольца поставь 4 чекпоинта — высокие
|
||
тонкие кубы-примитивы, материал «Неон». Первый сделай
|
||
зелёным (старт/финиш), остальные жёлтыми. «Столкновение»
|
||
выключено. Имена — «Чекпоинт_1» … «Чекпоинт_4».
|
||
</Step>
|
||
<Step n="3">
|
||
Точку спавна поставь у первого (зелёного) чекпоинта.
|
||
</Step>
|
||
<Shot src="lesson32-scene.png"
|
||
caption="Кольцевая трасса с четырьмя чекпоинтами" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ГОНКА С КРУГАМИ» — главный скрипт ===
|
||
|
||
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);
|
||
}
|
||
}
|
||
});`}</Code>
|
||
<p>Главное здесь — порядок чекпоинтов:</p>
|
||
<ul>
|
||
<li><code>game.onMessage('checkpoint', (d) => {'{...}'})</code> —
|
||
ловит сообщение от чекпоинта. Каждый чекпоинт — отдельный
|
||
скрипт в своей «песочнице», поэтому номер приходит
|
||
в «посылке» <code>d.num</code>, а не через общую
|
||
переменную;</li>
|
||
<li><code>nextCp</code> — номер чекпоинта, который мы
|
||
ждём следующим (начинаем с 0);</li>
|
||
<li><code>if (n - 1 !== nextCp) return</code> — если
|
||
игрок пересёк <b>не тот</b> чекпоинт (например,
|
||
проехал второй раньше первого) — пропускаем, он
|
||
не засчитан;</li>
|
||
<li>пересёк правильный — <code>nextCp</code> сдвигается
|
||
дальше. Прошёл все 4 — <code>lap</code> (круг)
|
||
растёт, а <code>nextCp</code> сбрасывается в 0;</li>
|
||
<li><code>if (lap {'>'}= LAPS)</code> — проехал 2 круга,
|
||
секундомер останавливается, показывается время.</li>
|
||
</ul>
|
||
<Note>
|
||
Проверка порядка не даёт «срезать» — нельзя проехать
|
||
половину трассы и засчитать круг. Только все чекпоинты
|
||
по очереди дадут круг.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт чекпоинта</h3>
|
||
<p>
|
||
На каждый чекпоинт — свой скрипт, отличается только
|
||
номер. Вот скрипт первого чекпоинта:
|
||
</p>
|
||
<ScriptKind kind="object" on="Чекпоинт_1" />
|
||
<Code>{`// === Скрипт чекпоинта 1 ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('checkpoint', { num: 1 });
|
||
});`}</Code>
|
||
<p>
|
||
Чекпоинт шлёт сообщение
|
||
<code> game.broadcast('checkpoint', {'{ num: 1 }'})</code>:
|
||
имя сообщения — <code>'checkpoint'</code>, а «посылка»
|
||
с номером говорит главному скрипту, какую точку пересёк
|
||
игрок. На «Чекпоинт_2» — такой же скрипт с
|
||
<code> num: 2</code>, на «Чекпоинт_3» — с <code>num: 3</code>,
|
||
на «Чекпоинт_4» — с <code>num: 4</code>.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>с началом игры пошёл секундомер;</li>
|
||
<li>проезжай чекпоинты по кругу по порядку;</li>
|
||
<li>после 4 чекпоинтов засчитывается круг;</li>
|
||
<li>проехал 2 круга — «Финиш» и твоё время.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
увеличь число кругов — поменяй <code>LAPS</code> на 3.
|
||
Поставь на трассе препятствия. Покрась пройденный
|
||
чекпоинт в зелёный через <code>game.scene.setColor</code>,
|
||
чтобы было видно, где ты уже был.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 33 — «Платформер с боссом»
|
||
// ════════════════════════════════════════════════════
|
||
'boss-platformer': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Сначала паркур — допрыгай по платформам до высокой
|
||
арены. Там тебя ждёт NPC-босс с большим запасом
|
||
здоровья. Кликай по нему, пока полоска HP не обнулится.
|
||
Над боссом висит метка с его здоровьем.
|
||
</p>
|
||
<Shot src="lesson33-result.png"
|
||
caption="Паркур до арены, потом бой с боссом" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Паркур + бой</b> — как соединить две механики
|
||
в одной игре;</li>
|
||
<li><b>Спавн босса один раз</b> — создать NPC, когда
|
||
игрок дошёл до арены;</li>
|
||
<li><b>Метка над NPC</b> — показать HP босса
|
||
(<code>game.scene.setLabel</code>);</li>
|
||
<li><b>Событие смерти NPC</b> — <code>boss.onDeath</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Старт, паркур, арена</h3>
|
||
<Step n="1">
|
||
Поставь стартовую площадку из травы внизу.
|
||
</Step>
|
||
<Step n="2">
|
||
Расставь 4 платформы — кубы-примитивы, ступеньками вверх
|
||
к арене. «Столкновение» и «Закреплён» включены.
|
||
</Step>
|
||
<Step n="3">
|
||
Наверху выложи большую арену из камня — площадку, где
|
||
будет бой с боссом.
|
||
</Step>
|
||
<Shot src="lesson33-scene.png"
|
||
caption="Платформы паркура ведут к арене наверху" />
|
||
<p>
|
||
Босса заранее ставить не нужно — его создаёт скрипт,
|
||
когда игрок добирается до арены.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПЛАТФОРМЕР С БОССОМ» — главный скрипт ===
|
||
|
||
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');
|
||
}
|
||
});
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём по частям. Сначала — паркур:</p>
|
||
<ul>
|
||
<li><code>game.onTick</code> следит за падением: упал
|
||
ниже -3 — респаун;</li>
|
||
<li><code>if (!bossSpawned && p.z {'>'} 24 && p.y {'>'} 5)</code> —
|
||
как только игрок зашёл на арену (далеко по
|
||
<code> z</code> и высоко по <code>y</code>) —
|
||
создаём босса. Флажок <code>bossSpawned</code>
|
||
не даёт создать его второй раз.</li>
|
||
</ul>
|
||
<p>Потом — бой:</p>
|
||
<ul>
|
||
<li><code>boss.follow('player')</code> — босс гонится
|
||
за игроком;</li>
|
||
<li><code>game.scene.setLabel(boss.ref, текст, опции)</code> —
|
||
вешает над боссом метку-табличку с его HP. Это
|
||
billboard — она всегда повёрнута к камере;</li>
|
||
<li><code>game.onClick</code> — клик по боссу, если игрок
|
||
близко: <code>bossHp</code> уменьшается на 20,
|
||
<code> boss.damage(20)</code> ранит NPC, метка
|
||
обновляется;</li>
|
||
<li><code>boss.onDeath(fn)</code> — когда у босса HP
|
||
дошло до нуля, эта функция выполнится: победа,
|
||
метку убираем через <code>clearLabel</code>.</li>
|
||
</ul>
|
||
<Note>
|
||
Босс <code>hp: 120</code>, удар снимает 20 — значит
|
||
нужно 6 точных кликов. Меняй эти числа, чтобы сделать
|
||
бой легче или сложнее.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Проверка</h3>
|
||
<ul>
|
||
<li>пройди паркур по платформам до арены;</li>
|
||
<li>на арене появится босс с меткой HP;</li>
|
||
<li>подбегай и кликай — HP босса падает;</li>
|
||
<li>HP дошло до нуля — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай босса сильнее — больше HP или быстрее. Добавь
|
||
второго мини-босса. Сделай, чтобы босс тоже наносил
|
||
игроку урон при касании — паркур обратно за аптечкой.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 34 — «Сбор урожая»
|
||
// ════════════════════════════════════════════════════
|
||
'harvest': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
На грядках растут шесть растений. Каждое медленно растёт
|
||
(анимация-твин) и за 5 секунд становится спелым — жёлтым.
|
||
Только спелое растение можно собрать клавишей
|
||
<kbd className="kbd">E</kbd>. Собрал слишком рано —
|
||
не считается. Собери весь урожай.
|
||
</p>
|
||
<Shot src="lesson34-result.png"
|
||
caption="Дождись, пока растение поспеет, и собирай" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Твин роста</b> — как плавно увеличить объект
|
||
(<code>game.tween</code> по размеру);</li>
|
||
<li><b>Событие <code>onDone</code></b> — сделать что-то,
|
||
когда твин закончился;</li>
|
||
<li><b>Состояние «спелое»</b> — флажок, который меняет
|
||
правила взаимодействия.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Грядки и растения</h3>
|
||
<Step n="1">
|
||
Выложи поле из блоков земли — это грядки.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь 6 растений — конусы-примитивы, изначально
|
||
<b> маленькие</b> (например, размер 0.4×0.5×0.4),
|
||
зелёного цвета. «Столкновение» включено. Имена —
|
||
«Растение_1» … «Растение_6».
|
||
</Step>
|
||
<Shot src="lesson34-scene.png"
|
||
caption="Шесть маленьких растений на грядках" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «СБОР УРОЖАЯ» — главный скрипт ===
|
||
|
||
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 });
|
||
}
|
||
});`}</Code>
|
||
<p>
|
||
Главный скрипт считает собранные растения и проверяет
|
||
победу. Каждое растение работает в своей «песочнице»,
|
||
поэтому о сборе оно сообщает через
|
||
<code> game.broadcast('harvested')</code>, а главный
|
||
скрипт ловит сообщение через
|
||
<code> game.onMessage('harvested', ...)</code>. Вся
|
||
«грядочная магия» — в скрипте растения.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт растения</h3>
|
||
<p>Этот скрипт вешается на <b>каждое</b> растение.</p>
|
||
<ScriptKind kind="object" on="каждое растение" />
|
||
<Code>{`// === Скрипт растения ===
|
||
|
||
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 });`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.tween(ref, {'{ sx, sy, sz, y }'}, опции)</code> —
|
||
твин по размеру. Растение плавно увеличивается
|
||
с маленького до большого за 5 секунд. Заодно растёт
|
||
и <code>y</code> — чтобы низ конуса оставался на земле,
|
||
а не уходил под неё;</li>
|
||
<li><code>onDone: () => {'{...}'}</code> — функция,
|
||
которая выполнится, <b>когда твин закончится</b>.
|
||
Внутри: ставим <code>ripe = true</code> (растение
|
||
спелое) и красим его в жёлтый;</li>
|
||
<li><code>game.self.onInteract</code> — сбор по
|
||
<kbd className="kbd">E</kbd>. Если
|
||
<code> !ripe</code> — растение ещё не поспело,
|
||
показываем подсказку и выходим;</li>
|
||
<li>спелое — <code>picked = true</code>, растение
|
||
исчезает и засчитывается.</li>
|
||
</ul>
|
||
<Note>
|
||
Флажок <code>ripe</code> меняет правила: до того как он
|
||
стал <code>true</code>, собрать растение нельзя. Это
|
||
и есть «состояние» объекта.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>с началом игры растения начинают расти;</li>
|
||
<li>попробуй собрать раннее — «Ещё не выросло!»;</li>
|
||
<li>через 5 секунд растение пожелтело — спелое;</li>
|
||
<li>собери все 6 спелых — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай растения с разным временем роста — одни за 3
|
||
секунды, другие за 8. Добавь «перезрелое» состояние:
|
||
если не собрать вовремя, растение чернеет и пропадает.
|
||
Поставь грядок побольше.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 35 — «Прятки от NPC»
|
||
// ════════════════════════════════════════════════════
|
||
'hide-from-npc': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
По полю ходит NPC-искатель и постоянно идёт к игроку.
|
||
Среди поля расставлены стены-укрытия. Нужно петлять
|
||
за ними и не дать себя поймать целых 40 секунд. Поймал —
|
||
игрок возвращается на старт, но таймер не сбрасывается.
|
||
</p>
|
||
<Shot src="lesson35-result.png"
|
||
caption="Искатель ищет тебя — прячься 40 секунд" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>NPC-преследователь</b> —
|
||
<code> spawnNpc</code> + <code>follow('player')</code>;</li>
|
||
<li><b>Таймер выживания</b> — продержаться нужное
|
||
количество секунд (<code>game.ui.timer</code> +
|
||
<code> dt</code>);</li>
|
||
<li><b>Проверка поимки</b> — <code>Math.hypot</code>
|
||
между игроком и NPC.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле и укрытия</h3>
|
||
<Step n="1">
|
||
Выложи большое травяное поле — простор, чтобы было
|
||
где убегать.
|
||
</Step>
|
||
<Step n="2">
|
||
Расставь по полю 7 стен-укрытий — серые кубы-примитивы
|
||
4×4×1.5. «Столкновение» и «Закреплён» включены.
|
||
Расположи их так, чтобы вокруг каждого можно было
|
||
обежать.
|
||
</Step>
|
||
<Step n="3">
|
||
Точку спавна поставь в углу поля.
|
||
</Step>
|
||
<p>
|
||
NPC-искатель создаётся скриптом — ставить его заранее
|
||
не нужно.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>Вся игра — в одном скрипте. Разберём его.</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПРЯТКИ ОТ 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 });
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.scene.spawnNpc(...)</code> создаёт
|
||
искателя, <code>seeker.follow('player')</code> —
|
||
он гонится за игроком;</li>
|
||
<li><code>time = time + dt</code> каждый кадр копит
|
||
прошедшее время, <code>game.ui.timer</code> выводит
|
||
секундомер;</li>
|
||
<li><code>Math.hypot(...) {'<'} 1.7</code> — искатель
|
||
вплотную. Игрок возвращается на старт, но
|
||
<code> time</code> не обнуляется — таймер идёт
|
||
дальше;</li>
|
||
<li><code>if (time {'>'}= SURVIVE)</code> — продержался
|
||
40 секунд: <code>seeker.stop()</code> останавливает
|
||
искателя, показывается «Победа».</li>
|
||
</ul>
|
||
<Note>
|
||
Хитрость игры: при поимке таймер <b>не сбрасывается</b>.
|
||
Игрока просто отбрасывает на старт. Поэтому даже если
|
||
раз поймали — до победы всё равно недалеко.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Проверка</h3>
|
||
<ul>
|
||
<li>с началом игры за тобой ходит искатель;</li>
|
||
<li>петляй вокруг серых стен, чтобы он не догнал;</li>
|
||
<li>поймал — респаун на старте, таймер идёт дальше;</li>
|
||
<li>продержался 40 секунд — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь второго искателя — игра станет напряжённее.
|
||
Сделай искателя быстрее или удлини время выживания.
|
||
Поставь больше укрытий — или, наоборот, убери часть,
|
||
чтобы прятаться было сложнее.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 36 — «Головоломка с ящиками»
|
||
// ════════════════════════════════════════════════════
|
||
'box-puzzle': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
На поле стоят 3 коричневых ящика и 3 зелёные плиты-цели.
|
||
Игрок подходит к ящику и нажимает <kbd className="kbd">E</kbd>
|
||
— ящик плавно перепрыгивает на следующую клетку. Задача —
|
||
расставить все ящики по их плитам. Получилось — победа.
|
||
</p>
|
||
<Shot src="lesson36-result.png"
|
||
caption="Головоломка: ящики нужно поставить на зелёные плиты" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Твин-перемещение</b> — как плавно сдвигать объект
|
||
из точки в точку (<code>game.tween</code>);</li>
|
||
<li><b>Список позиций</b> — как хранить ряд клеток в массиве
|
||
и ходить по нему по кругу;</li>
|
||
<li><b>Проверка состояния</b> — как понять, что все три
|
||
ящика стоят на своих местах;</li>
|
||
<li><b>Несколько объектов с одной логикой</b> — каждый ящик
|
||
знает свой номер.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле, плиты и ящики</h3>
|
||
<Step n="1">
|
||
Построй ровную площадку из камня примерно 18×18 блоков.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь 3 примитива-<b>цилиндра</b> в ряд впереди: размер
|
||
2.4×0.2×2.4, цвет зелёный <code>#22dd55</code>, материал
|
||
«Неон». Это плиты-цели. «Столкновение» — выключи, чтобы
|
||
ящик мог встать прямо на плиту.
|
||
</Step>
|
||
<Step n="3">
|
||
Поставь 3 примитива-<b>куба</b> размером 1.8 — это ящики.
|
||
Цвет коричневый <code>#b5651d</code>. Каждый ящик — напротив
|
||
своей плиты, но в дальнем ряду. Дай имена «Ящик_1»,
|
||
«Ящик_2», «Ящик_3».
|
||
</Step>
|
||
<Shot src="lesson36-scene.png"
|
||
caption="Три ящика в дальнем ряду, три зелёные плиты у игрока" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт хранит, какие ящики уже на плитах, и ловит
|
||
момент победы.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ГОЛОВОЛОМКА С ЯЩИКАМИ» — главный скрипт ===
|
||
|
||
// для каждого ящика — на какой плите он сейчас (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 });
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>onPlate</code> — массив из трёх «галочек»: стоит
|
||
ли ящик №0, №1, №2 на плите;</li>
|
||
<li><code>game.onMessage('box', ...)</code> — главный скрипт
|
||
ждёт сообщение «box» от ящиков. В <code>d.i</code> —
|
||
номер ящика, в <code>d.on</code> — стоит ли он на плите;</li>
|
||
<li><code>if (onPlate[0] {'&&'} onPlate[1] {'&&'} onPlate[2])</code>
|
||
— победа, только когда все три «галочки» стоят разом.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт ящика</h3>
|
||
<p>
|
||
Скрипт висит на ящике. Каждый ящик ходит по своему ряду
|
||
из 5 клеток. Тут показан ящик №1 (для второго и третьего
|
||
поменяй число <code>i</code> в сообщении на 1 и 2).
|
||
</p>
|
||
<ScriptKind kind="object" on="каждый ящик" />
|
||
<Code>{`// === Скрипт ящика 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 });`}</Code>
|
||
<p>Что происходит:</p>
|
||
<ul>
|
||
<li><code>ROW</code> — пять клеток ряда (значения Z);</li>
|
||
<li><code>cell = (cell + 1) % ROW.length</code> — переходим
|
||
к следующей клетке; <code>%</code> заворачивает с конца
|
||
ряда обратно к началу;</li>
|
||
<li><code>game.tween(...)</code> — плавно сдвигает ящик
|
||
на новую клетку за 0.4 секунды;</li>
|
||
<li><code>z === PLATE_Z</code> — проверка «ящик на плите?»:
|
||
результат (<code>true</code>/<code>false</code>) уходит
|
||
в поле <code>on</code> сообщения;</li>
|
||
<li><code>game.broadcast('box', ...)</code> — шлёт сообщение
|
||
«box» главному скрипту, а тот ловит его через
|
||
<code> game.onMessage('box', ...)</code>.</li>
|
||
</ul>
|
||
<Note>
|
||
У каждого ящика свой номер <code>i</code>: первый шлёт
|
||
<code> {'{'} i: 0 {'}'}</code>, второй — <code>{'{'} i: 1 {'}'}</code>,
|
||
третий — <code>{'{'} i: 2 {'}'}</code>. Не перепутай числа!
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>подойди к ящику, нажми <kbd className="kbd">E</kbd> —
|
||
ящик плавно прыгает вперёд;</li>
|
||
<li>доведи каждый ящик до его зелёной плиты;</li>
|
||
<li>все три на местах — «Победа» и конфетти.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь четвёртый ящик и плиту. Сделай ряды длиннее —
|
||
головоломка станет дольше. Поставь ящикам разные цвета и
|
||
плиты в тон — пусть каждый ящик ищет «свой» цвет.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 37 — «Полоса препятствий»
|
||
// ════════════════════════════════════════════════════
|
||
'obstacle-course': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Длинная трасса с препятствиями: красные шипы наносят урон,
|
||
в полу — ямы, над одной ямой ездит синяя платформа. Посередине
|
||
стоит жёлтый чекпоинт — пройдёшь его, и после падения игра
|
||
вернёт тебя сюда, а не на старт. В конце — зелёный финиш.
|
||
</p>
|
||
<Shot src="lesson37-result.png"
|
||
caption="Полоса препятствий: шипы, ямы, движущаяся платформа" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Урон от препятствия</b> — как отнять у игрока
|
||
здоровье (<code>game.player.damage</code>);</li>
|
||
<li><b>Движущаяся платформа</b> — твин «туда-обратно»
|
||
с <code>yoyo: true</code>;</li>
|
||
<li><b>Чекпоинт</b> — как перенести точку возрождения
|
||
(<code>game.player.setSpawn</code>);</li>
|
||
<li><b>Падение в яму</b> — возврат игрока через
|
||
<code> onTick</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Трасса с ямами</h3>
|
||
<Step n="1">
|
||
Построй длинную дорожку из камня — 8 блоков в ширину,
|
||
около 50 в длину.
|
||
</Step>
|
||
<Step n="2">
|
||
Вырежи в полу две <b>ямы</b> — просто не клади блоки в этих
|
||
местах. Игрок, который провалится, упадёт вниз.
|
||
</Step>
|
||
<Step n="3">
|
||
Расставь по трассе примитивы-<b>конусы</b> красного цвета —
|
||
это шипы. Размер 1.2×1.6×1.2, материал «Неон», «Столкновение»
|
||
выключи (касание ловит скрипт).
|
||
</Step>
|
||
<Step n="4">
|
||
Над первой ямой поставь синий куб «ДвижПлатформа» (3×0.5×3),
|
||
над второй — узкий коричневый «Мостик». Посередине трассы —
|
||
жёлтый конус «Чекпоинт». В конце — зелёный куб «Финиш».
|
||
</Step>
|
||
<Shot src="lesson37-scene.png"
|
||
caption="Трасса с шипами, ямами, платформой и чекпоинтом" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПОЛОСА ПРЕПЯТСТВИЙ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.after(0.2, ...)</code> — ждём чуть-чуть,
|
||
потому что <code>findOne</code> в самом начале ещё не
|
||
видит объекты сцены;</li>
|
||
<li><code>game.tween(mover, ...)</code> с <code>yoyo: true</code>
|
||
и <code>repeat: 999</code> — платформа бесконечно
|
||
ездит туда и обратно;</li>
|
||
<li><code>onTick</code> ловит падение в яму
|
||
(<code>p.y {'<'} -3</code>) и возвращает игрока;</li>
|
||
<li><code>game.onMessage('checkpoint', ...)</code> ловит
|
||
сообщение от чекпоинта и зовёт
|
||
<code> game.player.setSpawn(...)</code> — теперь
|
||
респаун будет в середине трассы;</li>
|
||
<li><code>game.onMessage('finish', ...)</code> — победа
|
||
с конфетти, флажок <code>won</code> защищает от повтора.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипты шипа, чекпоинта и финиша</h3>
|
||
<ScriptKind kind="object" on="каждый красный шип" />
|
||
<Code>{`// === Скрипт шипа ===
|
||
game.self.onTouch(() => {
|
||
game.player.damage(25);
|
||
game.sound.play('hit');
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="жёлтый чекпоинт" />
|
||
<Code>{`// === Скрипт чекпоинта ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('checkpoint');
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish');
|
||
});`}</Code>
|
||
<p>
|
||
Шип при касании отнимает 25 здоровья. Чекпоинт сохраняет
|
||
точку возрождения. Финиш зовёт победу.
|
||
</p>
|
||
<Note>
|
||
<code>setSpawn</code> меняет место, куда вернёт
|
||
<code> respawn</code>. До чекпоинта это старт, после —
|
||
середина трассы. Так игроку не приходится бежать заново.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>наступил на шип — здоровье уменьшилось;</li>
|
||
<li>упал в яму — вернуло на старт (или на чекпоинт);</li>
|
||
<li>прошёл жёлтый конус — респаун теперь посередине;</li>
|
||
<li>дошёл до зелёного финиша — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь второй чекпоинт ближе к финишу. Сделай шипы,
|
||
которые двигаются (твин). Добавь монетки на сложных
|
||
участках — награда за риск.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 38 — «Музыкальная игра»
|
||
// ════════════════════════════════════════════════════
|
||
'music-game': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
На поле 4 цветные плитки-ноты. Игра сначала проигрывает
|
||
мелодию из 5 нот — слушай внимательно. Потом нужно повторить
|
||
её, нажимая плитки <kbd className="kbd">E</kbd> в том же
|
||
порядке. Ошибся — мелодия начинается сначала.
|
||
</p>
|
||
<Shot src="lesson38-result.png"
|
||
caption="Четыре ноты-плитки: запомни мелодию и повтори" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Последовательность</b> — как хранить мелодию
|
||
в массиве чисел;</li>
|
||
<li><b>Отложенные события</b> — как проиграть ноты одну
|
||
за другой через <code>game.after</code>;</li>
|
||
<li><b>Сравнение с эталоном</b> — как проверить, что игрок
|
||
нажимает в правильном порядке;</li>
|
||
<li><b>Разные звуки</b> — у каждой плитки свой
|
||
<code> game.sound</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле и 4 ноты</h3>
|
||
<Step n="1">
|
||
Построй небольшую площадку из блоков (примерно 16×13).
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь 4 примитива-<b>куба</b> в ряд: размер 2.4×0.4×2.4,
|
||
материал «Неон». Цвета — красный, жёлтый, зелёный, синий.
|
||
Имена «Нота_1»…«Нота_4». «Столкновение» включено.
|
||
</Step>
|
||
<Shot src="lesson38-scene.png"
|
||
caption="Ряд из четырёх цветных нот-плиток" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт хранит мелодию, проигрывает её и проверяет,
|
||
верно ли игрок повторяет.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «МУЗЫКАЛЬНАЯ ИГРА» — главный скрипт ===
|
||
|
||
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');
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>SEQ = [1, 3, 2, 4, 1]</code> — загаданная мелодия:
|
||
номера нот по порядку;</li>
|
||
<li><code>SEQ.forEach((note, i) ={'>'} ...)</code> — для
|
||
каждой ноты ставим таймер: нота <code>i</code> прозвучит
|
||
через <code>1 + i * 0.8</code> секунд;</li>
|
||
<li><code>canPress</code> — флажок: нажимать можно только
|
||
после того, как мелодия доиграла;</li>
|
||
<li><code>game.onMessage('press', ...)</code> ловит сообщение
|
||
от плитки; <code>d.n</code> — номер нажатой ноты;</li>
|
||
<li><code>if (d.n === SEQ[playerStep])</code> — нажатая нота
|
||
совпала с нужной? Да — шаг вперёд; нет — сброс
|
||
<code> playerStep</code> в ноль.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт ноты-плитки</h3>
|
||
<p>
|
||
Скрипт висит на каждой плитке. Тут показана нота №1 —
|
||
для остальных поменяй звук и число в <code>press</code>.
|
||
</p>
|
||
<ScriptKind kind="object" on="каждую ноту-плитку" />
|
||
<Code>{`// === Скрипт ноты-плитки 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 });`}</Code>
|
||
<p>
|
||
При нажатии <kbd className="kbd">E</kbd> плитка играет
|
||
свой звук, вспыхивает искрами и шлёт
|
||
<code> game.broadcast('press', {'{'} n: 1 {'}'})</code> —
|
||
сообщение со своим номером. У ноты №2 будет
|
||
<code> {'{'} n: 2 {'}'}</code>, у №3 —
|
||
<code> {'{'} n: 3 {'}'}</code> и т.д.
|
||
</p>
|
||
<Note>
|
||
Цвет искр в <code>spawnParticles</code> и звук должны
|
||
совпадать с цветом плитки — так игроку понятнее, что
|
||
он нажал.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>в начале игра сама проигрывает 5 нот по очереди;</li>
|
||
<li>дождись надписи «Теперь повтори мелодию»;</li>
|
||
<li>нажимай плитки в том же порядке;</li>
|
||
<li>повторил всё верно — «Победа», ошибся — сброс.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
удлини мелодию <code>SEQ</code> до 7-8 нот. Сделай 5-ю
|
||
плитку. Добавь подсветку: пусть плитка на миг меняет цвет,
|
||
когда игра её проигрывает.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 39 — «Башня — стройка»
|
||
// ════════════════════════════════════════════════════
|
||
'tower-build': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Над землёй висят 8 полупрозрачных «призрачных» блоков один
|
||
над другим. Игрок подходит к призраку и нажимает
|
||
<kbd className="kbd">E</kbd> — блок становится настоящим:
|
||
плотным и коричневым. Строить нужно строго снизу вверх.
|
||
Построил всю башню — победа.
|
||
</p>
|
||
<Shot src="lesson39-result.png"
|
||
caption="Башня собрана из 8 блоков снизу вверх" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Призрак-блок</b> — стеклянный примитив, который
|
||
нельзя задеть;</li>
|
||
<li><b>Превращение объекта</b> — как сделать призрак твёрдым
|
||
и непрозрачным (<code>passThrough</code>,
|
||
<code> setOpacity</code>, <code>setCollide</code>);</li>
|
||
<li><b>Порядок действий</b> — проверка «следующий по
|
||
очереди»;</li>
|
||
<li><b>Счётчик стройки</b> — сколько блоков уже поставлено.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле и места-призраки</h3>
|
||
<Step n="1">
|
||
Построй площадку из травы (16×16 блоков).
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь примитив-<b>куб</b> размером 2×2×2 на полу. Цвет
|
||
светло-синий <code>#88aaff</code>, материал
|
||
<b> «Стекло»</b> — куб станет полупрозрачным.
|
||
«Столкновение» выключи: сквозь призрак можно пройти.
|
||
</Step>
|
||
<Step n="3">
|
||
Дублируй куб 7 раз и поставь их <b>стопкой друг над другом</b>
|
||
— каждый следующий на 2 единицы выше. Имена
|
||
«Место_1»…«Место_8», снизу вверх.
|
||
</Step>
|
||
<Shot src="lesson39-scene.png"
|
||
caption="Стопка из восьми полупрозрачных мест-призраков" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «БАШНЯ — СТРОЙКА» — главный скрипт ===
|
||
|
||
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);
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>placed</code> — сколько блоков уже стоит;</li>
|
||
<li><code>game.onMessage('place', ...)</code> ловит сообщение
|
||
от места-призрака; <code>d.n</code> — его номер;</li>
|
||
<li><code>if (d.n !== placed + 1)</code> — если игрок жмёт
|
||
не на следующий по очереди блок, стройка не засчитывается;</li>
|
||
<li><code>game.ui.score = placed</code> — счётчик показывает
|
||
прогресс башни;</li>
|
||
<li>когда <code>placed</code> дошёл до 8 — победа.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт места-призрака</h3>
|
||
<p>
|
||
Скрипт висит на каждом призраке. Тут показано место №1 —
|
||
у остальных поменяй число в <code>place</code>.
|
||
</p>
|
||
<ScriptKind kind="object" on="каждое место-призрак" />
|
||
<Code>{`// === Скрипт места под блок 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 });`}</Code>
|
||
<p>Что происходит при нажатии:</p>
|
||
<ul>
|
||
<li><code>passThrough(ref, false)</code> — сквозь блок
|
||
больше нельзя пройти;</li>
|
||
<li><code>setOpacity(ref, 1)</code> — блок становится
|
||
полностью непрозрачным;</li>
|
||
<li><code>setColor(ref, '#b5651d')</code> — перекрашиваем
|
||
в коричневый «деревянный»;</li>
|
||
<li><code>setCollide(ref, true)</code> — теперь на блок
|
||
можно встать.</li>
|
||
</ul>
|
||
<Note>
|
||
У каждого места свой номер <code>n</code> в сообщении:
|
||
место_1 шлёт <code>{'{'} n: 1 {'}'}</code>, место_2 —
|
||
<code> {'{'} n: 2 {'}'}</code> и так до 8.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>подойди к нижнему призраку, нажми
|
||
<kbd className="kbd">E</kbd> — он стал коричневым;</li>
|
||
<li>встань на новый блок и ставь следующий выше;</li>
|
||
<li>если жмёшь не по порядку — подсказка «сначала ниже»;</li>
|
||
<li>построил все 8 — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
сделай башню выше — 12-15 блоков. Добавь блоки разного
|
||
цвета. Поставь монетку на самом верху как награду
|
||
за постройку.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 40 — «Выживание от волн»
|
||
// ════════════════════════════════════════════════════
|
||
'wave-survival': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Открытая арена. На игрока накатывают 3 волны врагов-NPC,
|
||
одна за другой. С каждой волной врагов всё больше. Кликай
|
||
по врагам, чтобы их уничтожать. Отбил все 3 волны — победа.
|
||
</p>
|
||
<Shot src="lesson40-result.png"
|
||
caption="Враги наступают волнами — отбей все три" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>NPC-враги</b> — как создавать персонажей-врагов
|
||
(<code>spawnNpc</code>) и заставлять их преследовать;</li>
|
||
<li><b>Волны</b> — как запускать врагов группами;</li>
|
||
<li><b>Рекурсивный вызов</b> — как функция волны запускает
|
||
саму себя для следующей волны;</li>
|
||
<li><b>Клик по цели</b> — проверка расстояния при
|
||
<code> game.onClick</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Арена</h3>
|
||
<Step n="1">
|
||
Построй большую ровную площадку из камня (примерно 24×24
|
||
блока). Стены не нужны — врагов лучше видно на открытом поле.
|
||
</Step>
|
||
<Step n="2">
|
||
Поставь точку спавна игрока в центр арены.
|
||
</Step>
|
||
<Note>
|
||
Враги создаются прямо в скрипте функцией
|
||
<code> spawnNpc</code> — заранее ставить их не нужно.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Вся игра — в одном глобальном скрипте: он запускает волны
|
||
и считает врагов.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ВЫЖИВАНИЕ ОТ ВОЛН» — главный скрипт ===
|
||
|
||
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 секунды`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>startWave()</code> — функция одной волны. Она
|
||
создаёт <code>wave + 2</code> врагов: 3, потом 4, потом 5;</li>
|
||
<li><code>Math.cos / Math.sin</code> — расставляют врагов
|
||
по кругу вокруг арены;</li>
|
||
<li><code>enemy.follow('player')</code> — враг идёт
|
||
к игроку;</li>
|
||
<li><code>game.onClick(...)</code> — при клике проверяем
|
||
<code> Math.hypot(...) {'<'} 5</code>: враг рядом — он
|
||
уничтожен;</li>
|
||
<li><code>if (aliveInWave {'<='} 0) ... game.after(2, startWave)</code>
|
||
— вся волна перебита, через 2 секунды функция
|
||
<b> запускает сама себя</b> для следующей волны. Это
|
||
и есть рекурсия.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Проверка</h3>
|
||
<ul>
|
||
<li>через 2 секунды появляется первая волна из 3 врагов;</li>
|
||
<li>кликай по врагам рядом — они взрываются;</li>
|
||
<li>перебил всю волну — через 2 секунды идёт следующая;</li>
|
||
<li>отбил 3 волны — «Победа» и конфетти.</li>
|
||
</ul>
|
||
<Note>
|
||
Рекурсия — это когда функция вызывает саму себя.
|
||
Здесь <code>startWave</code> запускает <code>startWave</code>
|
||
снова, пока не кончатся волны. Главное — есть условие
|
||
остановки (<code>wave {'>='} WAVES</code>), иначе волны
|
||
шли бы бесконечно.
|
||
</Note>
|
||
|
||
<Try>
|
||
сделай 5 волн вместо 3. Дай врагам больше здоровья
|
||
(<code>hp</code>) или скорости (<code>speed</code>) —
|
||
станет сложнее. Добавь между волнами «передышку» подлиннее.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 41 — «Платформер-приключение»
|
||
// ════════════════════════════════════════════════════
|
||
'adventure-platformer': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Большой уровень-приключение: длинная цепочка платформ-паркура,
|
||
на пути — золотые монетки, посередине жёлтый чекпоинт,
|
||
а на самом верху — финиш-сокровище. Эта игра собирает вместе
|
||
почти всё, что ты прошёл в простых уроках.
|
||
</p>
|
||
<Shot src="lesson41-result.png"
|
||
caption="Большой платформер: паркур, монетки, чекпоинт, сокровище" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Собирать игру из кусочков</b> — как соединить паркур,
|
||
монетки, чекпоинт и финиш в один уровень;</li>
|
||
<li><b>Большой главный скрипт</b> — один скрипт ловит
|
||
сообщения от всех объектов игры;</li>
|
||
<li><b>Несколько целей сразу</b> — добраться до финиша
|
||
И собрать монетки.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Большой уровень</h3>
|
||
<Step n="1">
|
||
Поставь стартовую площадку из травы внизу.
|
||
</Step>
|
||
<Step n="2">
|
||
Выстрой цепочку платформ-кубов (2.5×0.5×2.5) вверх и вперёд
|
||
— около 9 штук, каждая чуть выше предыдущей.
|
||
</Step>
|
||
<Step n="3">
|
||
Посередине подъёма сделай площадку из камня — это зона
|
||
чекпоинта. Сверху — финишную площадку.
|
||
</Step>
|
||
<Step n="4">
|
||
Расставь золотые сферы-монетки над некоторыми платформами.
|
||
Поставь жёлтый конус «Чекпоинт» и золотой куб «Сокровище».
|
||
</Step>
|
||
<Shot src="lesson41-scene.png"
|
||
caption="Уровень: паркур со старта вверх к сокровищу" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ПЛАТФОРМЕР-ПРИКЛЮЧЕНИЕ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.onMessage('coin', ...)</code> — пришла
|
||
монетка, +1 к счёту;</li>
|
||
<li><code>game.onMessage('checkpoint', ...)</code> —
|
||
перенести точку возрождения наверх;</li>
|
||
<li><code>game.onMessage('treasure', ...)</code> — победа:
|
||
в надписи показываем, сколько монеток успел собрать;</li>
|
||
<li><code>onTick</code> — следит за падением, как
|
||
в уроках 2 и 37.</li>
|
||
</ul>
|
||
<Note>
|
||
Главный скрипт большой, но устроен просто: он ждёт три
|
||
разных сообщения. Каждый объект уровня шлёт нужное ему
|
||
через <code>game.broadcast</code>.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипты монетки, чекпоинта и сокровища</h3>
|
||
<ScriptKind kind="object" on="каждую монетку" />
|
||
<Code>{`// === Скрипт монетки ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('coin');
|
||
game.self.delete();
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="жёлтый чекпоинт" />
|
||
<Code>{`// === Скрипт чекпоинта ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('checkpoint');
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="золотое сокровище" />
|
||
<Code>{`// === Скрипт сокровища ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('treasure');
|
||
});`}</Code>
|
||
<p>
|
||
Монетка при касании засчитывается и исчезает. Чекпоинт
|
||
сохраняет место. Сокровище зовёт победу.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>прыгай по платформам вверх, собирай монетки;</li>
|
||
<li>упал — респаун (на старт или на чекпоинт);</li>
|
||
<li>прошёл чекпоинт — дальше начинаешь отсюда;</li>
|
||
<li>добрался до сокровища — «Победа» с числом монеток.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь шипы из урока 37 на сложные платформы. Сделай
|
||
врага-преследователя из урока 21. Добавь второй уровень
|
||
выше первого — длинное приключение.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 42 — «RPG-деревня»
|
||
// ════════════════════════════════════════════════════
|
||
'rpg-village': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Маленькая деревня с двумя жителями: старостой и кузнецом.
|
||
Поговори со старостой — он даст квест: найти потерянный
|
||
амулет за домом. Подними амулет и отнеси его кузнецу —
|
||
получишь награду. Это настоящая RPG с этапами квеста.
|
||
</p>
|
||
<Shot src="lesson42-result.png"
|
||
caption="RPG-деревня: квест от старосты к кузнецу" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>NPC-жители</b> — как создавать персонажей и заставлять
|
||
их говорить (<code>spawnNpc</code>, <code>say</code>);</li>
|
||
<li><b>Этапы квеста</b> — переменная <code>stage</code>,
|
||
которая помнит, на каком шаге игрок;</li>
|
||
<li><b>Инвентарь</b> — как класть и проверять предмет
|
||
(<code>game.inventory</code>);</li>
|
||
<li><b>Диалоги по условию</b> — NPC говорит разное в
|
||
зависимости от этапа.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Деревня</h3>
|
||
<Step n="1">
|
||
Построй большую травяную площадку (примерно 32×32 блока).
|
||
</Step>
|
||
<Step n="2">
|
||
Сложи два домика из блоков — один деревянный, другой
|
||
из красного кирпича.
|
||
</Step>
|
||
<Step n="3">
|
||
Поставь две тумбы-кубы рядом с местами NPC: «Староста»
|
||
у входа, «Кузнец» у кирпичного дома. Это «кнопки» для
|
||
разговора.
|
||
</Step>
|
||
<Step n="4">
|
||
За дальним домом спрячь примитив-<b>тор</b> (бублик)
|
||
фиолетового цвета — это «Амулет».
|
||
</Step>
|
||
<Shot src="lesson42-scene.png"
|
||
caption="Деревня с двумя домами и спрятанным амулетом" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «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);
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>stage</code> — переменная-этап: 0 начало,
|
||
1 ищем амулет, 2 несём кузнецу, 3 квест выполнен;</li>
|
||
<li><code>spawnNpc(...)</code> со <code>speed: 0</code> —
|
||
житель стоит на месте;</li>
|
||
<li><code>elder.say(...)</code> — над NPC появляется
|
||
реплика;</li>
|
||
<li><code>game.inventory.add / has / remove</code> —
|
||
кладём амулет в сумку, проверяем и забираем;</li>
|
||
<li><code>game.onMessage('elderTalk', ...)</code> и
|
||
<code> game.onMessage('smithTalk', ...)</code> говорят
|
||
разное в зависимости от <code>stage</code> — это
|
||
и есть «живой» квест.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипты NPC и амулета</h3>
|
||
<ScriptKind kind="object" on="тумбу старосты" />
|
||
<Code>{`// === Скрипт старосты ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('elderTalk');
|
||
}, { text: 'Поговорить со старостой', distance: 4 });`}</Code>
|
||
<ScriptKind kind="object" on="тумбу кузнеца" />
|
||
<Code>{`// === Скрипт кузнеца ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('smithTalk');
|
||
}, { text: 'Поговорить с кузнецом', distance: 4 });`}</Code>
|
||
<ScriptKind kind="object" on="фиолетовый амулет" />
|
||
<Code>{`// === Скрипт амулета ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('takeAmulet');
|
||
game.self.delete();
|
||
});`}</Code>
|
||
<Note>
|
||
Тумба-куб — это «кнопка разговора». Сам NPC создаётся
|
||
скриптом и стоит рядом с тумбой. Игрок жмёт
|
||
<kbd className="kbd">E</kbd> на тумбе — и NPC отвечает.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>поговори со старостой — он выдаст квест;</li>
|
||
<li>найди фиолетовый амулет за дальним домом, подними его;</li>
|
||
<li>отнеси амулет кузнецу, поговори с ним;</li>
|
||
<li>кузнец благодарит — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь третьего NPC с ещё одним предметом — цепочка
|
||
квестов станет длиннее. Сделай так, чтобы за квест
|
||
игрок получал монетки в счёт. Добавь жителям реплики
|
||
«просто так».
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 43 — «Гонка с препятствиями»
|
||
// ════════════════════════════════════════════════════
|
||
'obstacle-race': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Гоночная трасса на время. По дороге — синие плитки-бусты:
|
||
наступил, и игрок ускоряется. И красные шипы-ловушки:
|
||
задел — урон и замедление. Доберись до финиша как можно
|
||
быстрее, секундомер покажет твоё время.
|
||
</p>
|
||
<Shot src="lesson43-result.png"
|
||
caption="Гонка на время: синее ускоряет, красное мешает" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Скорость игрока</b> — как ускорять и замедлять
|
||
(<code>game.player.setSpeed</code>);</li>
|
||
<li><b>Временный эффект</b> — буст действует 3 секунды,
|
||
потом скорость возвращается;</li>
|
||
<li><b>Секундомер</b> — счёт времени через
|
||
<code> onTick</code> и <code>game.ui.timer</code>;</li>
|
||
<li><b>Два типа препятствий</b> — полезное и вредное.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Трасса</h3>
|
||
<Step n="1">
|
||
Построй длинную дорожку из камня — 8 блоков в ширину,
|
||
около 70 в длину, с бортиками по бокам.
|
||
</Step>
|
||
<Step n="2">
|
||
Расставь синие кубы-бусты (4×0.3×2, материал «Неон»,
|
||
«Столкновение» выключено) поперёк дороги — 3 штуки.
|
||
</Step>
|
||
<Step n="3">
|
||
Расставь красные конусы-шипы (1.2×1.6×1.2) — 5 штук,
|
||
в шахматном порядке слева-справа.
|
||
</Step>
|
||
<Step n="4">
|
||
В конце поставь зелёный куб «Финиш».
|
||
</Step>
|
||
<Shot src="lesson43-scene.png"
|
||
caption="Трасса с бустами и шипами" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «ГОНКА С ПРЕПЯТСТВИЯМИ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>onTick((dt) ={'>'} ...)</code> — <code>dt</code>
|
||
это секунды с прошлого кадра; складываем их в
|
||
<code> time</code> — получается секундомер;</li>
|
||
<li><code>game.onMessage('boost', ...)</code> —
|
||
<code> setSpeed(1.8)</code> делает игрока в 1.8 раза
|
||
быстрее, а <code>game.after(3, ...)</code> через
|
||
3 секунды возвращает обычную скорость
|
||
<code> setSpeed(1)</code>;</li>
|
||
<li><code>game.onMessage('spike', ...)</code> — отнимает
|
||
здоровье и наоборот замедляет до
|
||
<code> setSpeed(0.5)</code> на 1.5 секунды;</li>
|
||
<li><code>Math.round(time * 10) / 10</code> — округляем
|
||
время до десятых долей секунды.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипты буста, шипа и финиша</h3>
|
||
<ScriptKind kind="object" on="каждый синий буст" />
|
||
<Code>{`// === Скрипт буста ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('boost');
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="каждый красный шип" />
|
||
<Code>{`// === Скрипт шипа-ловушки ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('spike');
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish');
|
||
});`}</Code>
|
||
<Note>
|
||
<code>setSpeed</code> — множитель скорости. 1 — обычная,
|
||
1.8 — быстро, 0.5 — медленно. После эффекта всегда
|
||
возвращай <code>setSpeed(1)</code>, иначе игрок навсегда
|
||
останется быстрым или медленным.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>с началом игры идёт секундомер;</li>
|
||
<li>наехал на синюю плитку — ускорение на 3 секунды;</li>
|
||
<li>задел красный шип — урон и замедление;</li>
|
||
<li>домчал до финиша — игра покажет твоё время.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь несколько кругов, как в гонке из урока 32. Сделай
|
||
бусты-«стрелки», которые ещё и подбрасывают. Поставь рекорд
|
||
времени и попробуй его побить.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 44 — «Tower Defense»
|
||
// ════════════════════════════════════════════════════
|
||
'tower-defense': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Враги идут по каменной дороге к твоей синей базе. По бокам
|
||
дороги — площадки: подойди и нажми <kbd className="kbd">E</kbd>,
|
||
чтобы построить башню. Башни сами стреляют по врагам рядом.
|
||
Уничтожь 14 врагов, не пропустив 8 — победа.
|
||
</p>
|
||
<Shot src="lesson44-result.png"
|
||
caption="Tower Defense: ставь башни, защищай базу" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Враги по маршруту</b> — NPC идут от старта к базе
|
||
(<code>moveTo</code>);</li>
|
||
<li><b>Постройка башен</b> — нажатие <kbd className="kbd">E</kbd>
|
||
создаёт объект (<code>game.scene.spawn</code>);</li>
|
||
<li><b>Авто-атака</b> — башни сами бьют врагов рядом
|
||
через <code>game.every</code>;</li>
|
||
<li><b>Счёт побед и прорывов</b> — два условия конца игры.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Дорога, площадки и база</h3>
|
||
<Step n="1">
|
||
Построй большую травяную площадку. По центру выложи
|
||
каменную <b>дорогу</b> на 1 блок выше — по ней пойдут враги.
|
||
</Step>
|
||
<Step n="2">
|
||
По бокам дороги поставь 4 куба-площадки (3×2×3, серые) —
|
||
имена «Площадка_1»…«Площадка_4». Это места под башни.
|
||
</Step>
|
||
<Step n="3">
|
||
В конце дороги поставь большой синий куб «База».
|
||
</Step>
|
||
<Shot src="lesson44-scene.png"
|
||
caption="Дорога врагов, площадки под башни, синяя база" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<p>
|
||
Главный скрипт делает всё: спавнит врагов, заставляет башни
|
||
стрелять и проверяет, кто победил.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «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);
|
||
}
|
||
}
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>towers</code> и <code>enemies</code> — два списка:
|
||
где стоят башни и какие враги живы;</li>
|
||
<li><code>game.onMessage('addTower', ...)</code> ловит
|
||
сообщение от площадки и кладёт координаты башни
|
||
в список <code>towers</code>;</li>
|
||
<li>первый <code>game.every(2.2, ...)</code> — каждые
|
||
2.2 секунды выпускает врага и зовёт
|
||
<code> npc.moveTo(...)</code> — враг идёт к базе;</li>
|
||
<li>второй <code>game.every(0.8, ...)</code> — каждая башня
|
||
ищет врага ближе 7 единиц и бьёт его;</li>
|
||
<li>третий <code>game.every(0.5, ...)</code> — проверяет,
|
||
не дошёл ли враг до базы (<code>position.z {'>'} 40</code>);</li>
|
||
<li>победа — <code>killed {'>='} GOAL</code>, поражение —
|
||
<code> leaked {'>='} MAX_LEAK</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт площадки под башню</h3>
|
||
<ScriptKind kind="object" on="каждую площадку" />
|
||
<Code>{`// === Скрипт площадки под башню ===
|
||
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 });`}</Code>
|
||
<p>
|
||
При нажатии <kbd className="kbd">E</kbd> скрипт создаёт
|
||
жёлтый цилиндр-башню над площадкой и шлёт сообщение
|
||
<code> game.broadcast('addTower', ...)</code> с координатами
|
||
башни — главный скрипт ловит его, кладёт башню в список,
|
||
и она начинает стрелять.
|
||
</p>
|
||
<Note>
|
||
Сама «стрельба» — это не пули, а проверка расстояния
|
||
в <code>game.every</code>. Так делают почти все простые
|
||
Tower Defense: башня просто бьёт ближайшего врага по таймеру.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3.1. Башня — своя модель из редактора моделей</h3>
|
||
<p>
|
||
Жёлтый цилиндр-башня — это просто заглушка. Можно поставить
|
||
вместо неё <b>свою воксельную модель</b>, которую ты сделал
|
||
в редакторе моделей проекта.
|
||
</p>
|
||
<ol>
|
||
<li>В редакторе проекта открой панель <b>«Мои модели»</b>
|
||
и создай башню в редакторе воксельных моделей.</li>
|
||
<li>Узнай её <b>id</b> — он показан рядом с названием модели
|
||
в панели «Мои модели» (например <code>3</code>).</li>
|
||
<li>В скрипте площадки замени блок
|
||
<code> game.scene.spawn('primitive:cylinder', ...)</code>
|
||
на такой:</li>
|
||
</ol>
|
||
<Code>{`// ставим башню — своя воксельная модель (id = 3)
|
||
game.scene.spawn('user:3', {
|
||
x: pos.x, y: pos.y + 2, z: pos.z,
|
||
rotationY: 0,
|
||
});`}</Code>
|
||
<p>
|
||
У пользовательских моделей нет <code>sx/sy/sz</code> и
|
||
<code> color</code> — размер и цвет задаются в самом
|
||
редакторе модели. Из параметров только позиция и
|
||
<code> rotationY</code> (поворот по вертикальной оси).
|
||
Высоту <code>y</code> подбери под высоту своей модели,
|
||
чтобы она ровно встала на площадку.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>враги идут по дороге к базе;</li>
|
||
<li>подойди к площадке, нажми <kbd className="kbd">E</kbd>
|
||
— встаёт башня;</li>
|
||
<li>башня сама стреляет искрами по врагам рядом;</li>
|
||
<li>уничтожил 14 — победа, пропустил 8 — поражение.</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь больше площадок под башни. Сделай врагам больше
|
||
здоровья на поздних волнах. Покрась башни разными цветами
|
||
— «обычная» и «сильная».
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 45 — «Стрелялка-арена»
|
||
// ════════════════════════════════════════════════════
|
||
'arena-shooter': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Закрытая арена. Со всех сторон к игроку сбегаются враги-NPC.
|
||
Кликай по врагам — они гибнут. Но и враги бьют тебя, если
|
||
подошли вплотную: у игрока есть здоровье. Перебей 15 врагов,
|
||
не потеряв всё HP.
|
||
</p>
|
||
<Shot src="lesson45-result.png"
|
||
caption="Стрелялка-арена: отбивайся от врагов кликами" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>«Стрельба» кликом</b> — клик уничтожает врага
|
||
рядом по проверке расстояния;</li>
|
||
<li><b>Урон игроку</b> — враг бьёт по таймеру
|
||
(<code>game.every</code> + <code>damage</code>);</li>
|
||
<li><b>Отслеживание здоровья</b> — конец игры при
|
||
<code> onHpChange</code>;</li>
|
||
<li><b>Отмена таймера</b> — <code>game.cancel</code>,
|
||
когда враг умер.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Арена со стенами</h3>
|
||
<Step n="1">
|
||
Построй квадратную площадку из камня (примерно 26×26 блоков).
|
||
</Step>
|
||
<Step n="2">
|
||
По всему краю поставь стены в 3 блока высотой — чтобы враги
|
||
и игрок не убегали с арены.
|
||
</Step>
|
||
<Step n="3">
|
||
Поставь точку спавна игрока в центр.
|
||
</Step>
|
||
<Note>
|
||
Врагов снова создаёт скрипт — расставлять их заранее
|
||
не нужно. Поэтому у этой игры только скриншот результата.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт ===
|
||
|
||
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 });
|
||
}
|
||
}
|
||
});
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.onHpChange((e) ={'>'} ...)</code> —
|
||
срабатывает, когда здоровье игрока меняется. Если
|
||
<code> e.hp {'<='} 0</code> — поражение;</li>
|
||
<li><code>game.every(1.8, ...)</code> — каждые 1.8 секунды
|
||
появляется новый враг по краю арены и
|
||
<code> follow('player')</code> — идёт к игроку;</li>
|
||
<li><code>dmgTimer</code> — отдельный таймер: пока враг жив,
|
||
каждые 0.7 секунды проверяет, рядом ли он, и бьёт;</li>
|
||
<li><code>game.cancel(dmgTimer)</code> — когда враг умер,
|
||
его таймер урона выключается, чтобы не работал зря;</li>
|
||
<li>клик ближе 6 единиц к врагу — он уничтожен, +1 к счёту.</li>
|
||
</ul>
|
||
<Note>
|
||
Каждый враг — это маленький «механизм»: свой флажок
|
||
<code> dead</code>, свой таймер <code>dmgTimer</code>,
|
||
свой обработчик клика. Когда враг гибнет, его механизм
|
||
аккуратно выключается через <code>game.cancel</code>.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Проверка</h3>
|
||
<ul>
|
||
<li>враги сбегаются к тебе со всех сторон;</li>
|
||
<li>кликай по врагу рядом — он взрывается, +1 к счёту;</li>
|
||
<li>враг вплотную — отнимает здоровье;</li>
|
||
<li>перебил 15 — «Победа», потерял всё HP — «Поражение».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь «аптечки» — сферы, которые лечат при касании.
|
||
Сделай врагов сильнее (больше <code>hp</code> и
|
||
<code> speed</code>). Увеличь цель до 25 врагов.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 46 — «Кликер»
|
||
// ════════════════════════════════════════════════════
|
||
'clicker': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
В центре площадки — большой жёлтый куб. Кликай по нему,
|
||
и копятся очки. По бокам — две кнопки-улучшения: красная
|
||
добавляет силу клика, синяя включает авто-доход (очки
|
||
капают сами). Накопи 200 очков — победа.
|
||
</p>
|
||
<Shot src="lesson46-result.png"
|
||
caption="Кликер: жми куб, копи очки, покупай улучшения" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Клик по объекту</b> — <code>game.self.onClick</code>;</li>
|
||
<li><b>Экономика улучшений</b> — тратим очки, чтобы
|
||
зарабатывать больше;</li>
|
||
<li><b>Авто-доход</b> — очки сами растут через
|
||
<code> game.every</code>;</li>
|
||
<li><b>Несколько мест проверки победы</b> — общая функция
|
||
<code> checkWin</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Куб и кнопки</h3>
|
||
<Step n="1">
|
||
Построй небольшую площадку из блоков.
|
||
</Step>
|
||
<Step n="2">
|
||
В центр поставь большой жёлтый куб «Кликер» (3×3×3,
|
||
материал «Неон»).
|
||
</Step>
|
||
<Step n="3">
|
||
Слева поставь красный куб «УлучшениеСила» (2×2×2), справа
|
||
— синий куб «УлучшениеАвто».
|
||
</Step>
|
||
<Shot src="lesson46-scene.png"
|
||
caption="Куб-кликер в центре, две кнопки-улучшения по бокам" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «КЛИКЕР» — главный скрипт ===
|
||
|
||
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);
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>points</code> — очки, <code>perClick</code> —
|
||
сколько даёт один клик, <code>autoIncome</code> —
|
||
сколько капает само в секунду;</li>
|
||
<li><code>game.every(1, ...)</code> — каждую секунду
|
||
прибавляет <code>autoIncome</code> к очкам;</li>
|
||
<li><code>checkWin()</code> — общая функция: проверка победы
|
||
вызывается и из клика, и из авто-дохода;</li>
|
||
<li><code>game.onMessage('click', ...)</code> ловит клик
|
||
по кубу — +<code>perClick</code> очков;</li>
|
||
<li><code>game.onMessage('buyPower', ...)</code> — за 20 очков
|
||
добавляет +2 к силе клика;</li>
|
||
<li><code>game.onMessage('buyAuto', ...)</code> — за 40 очков
|
||
добавляет +3 к авто-доходу.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипты куба и кнопок</h3>
|
||
<ScriptKind kind="object" on="жёлтый куб-кликер" />
|
||
<Code>{`// === Скрипт куба-кликера ===
|
||
game.self.onClick(() => {
|
||
game.broadcast('click');
|
||
// куб слегка вспыхивает
|
||
game.scene.spawnParticles('sparks', game.self.position, { duration: 0.3 });
|
||
});`}</Code>
|
||
<ScriptKind kind="object" on="красную кнопку" />
|
||
<Code>{`// === Скрипт улучшения «сила клика» (20 очков) ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('buyPower');
|
||
}, { text: 'Купить +силу клика (20)', distance: 3 });`}</Code>
|
||
<ScriptKind kind="object" on="синюю кнопку" />
|
||
<Code>{`// === Скрипт улучшения «авто-доход» (40 очков) ===
|
||
game.self.onInteract(() => {
|
||
game.broadcast('buyAuto');
|
||
}, { text: 'Купить авто-доход (40)', distance: 3 });`}</Code>
|
||
<Note>
|
||
Главная идея кликера: сначала кликаешь руками, потом
|
||
покупаешь улучшения — и игра «играет сама». Это экономика:
|
||
тратишь очки, чтобы зарабатывать быстрее.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>кликай по жёлтому кубу — очки растут;</li>
|
||
<li>накопил 20 — купи силу клика, клики станут «жирнее»;</li>
|
||
<li>накопил 40 — купи авто-доход, очки капают сами;</li>
|
||
<li>дошёл до 200 — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
добавь третье улучшение — например, ещё дороже и мощнее.
|
||
Сделай цель больше (1000 очков). Покажи <code>perClick</code>
|
||
и <code>autoIncome</code> отдельной надписью через
|
||
<code> game.ui.set</code>.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 47 — «Квест-побег»
|
||
// ════════════════════════════════════════════════════
|
||
'escape-quest': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Игрок заперт в комнате. Чтобы выбраться, нужно найти
|
||
и нажать 3 красные кнопки. Две на виду, а третья спрятана
|
||
за ящиком — её надо обойти. Нажал все три — дверь поднимается,
|
||
и можно выйти к финишу.
|
||
</p>
|
||
<Shot src="lesson47-result.png"
|
||
caption="Квест-побег: найди 3 кнопки и открой дверь" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Закрытая комната</b> — стены с проёмом под дверь;</li>
|
||
<li><b>Состояние головоломки</b> — счётчик нажатых кнопок;</li>
|
||
<li><b>Открытие двери</b> — твин по достижении условия;</li>
|
||
<li><b>Спрятанный объект</b> — кнопка за ящиком, которую
|
||
надо найти.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Комната, кнопки, дверь</h3>
|
||
<Step n="1">
|
||
Построй пол и стены закрытой комнаты (примерно 20×20).
|
||
В одной стене оставь <b>проём</b> шириной 3 блока — под дверь.
|
||
</Step>
|
||
<Step n="2">
|
||
Закрой проём примитивом-кубом «Дверь» (1×4×3, коричневый).
|
||
</Step>
|
||
<Step n="3">
|
||
Поставь 3 цилиндра-кнопки красного цвета по углам комнаты.
|
||
Одну спрячь за большим кубом-ящиком.
|
||
</Step>
|
||
<Step n="4">
|
||
За дверью снаружи поставь зелёный куб «Финиш».
|
||
</Step>
|
||
<Shot src="lesson47-scene.png"
|
||
caption="Запертая комната с тремя кнопками и ящиком" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт ===
|
||
|
||
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 });
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>pressed</code> — счётчик нажатых кнопок,
|
||
<code> TOTAL</code> — сколько их всего;</li>
|
||
<li><code>game.onMessage('pressButton', ...)</code> ловит
|
||
сообщение, которое кнопка шлёт при нажатии;</li>
|
||
<li><code>if (pressed {'>='} TOTAL)</code> — когда нажаты
|
||
все три, находим дверь по имени и поднимаем её твином
|
||
вверх;</li>
|
||
<li><code>game.onMessage('escape', ...)</code> — победа,
|
||
когда игрок прошёл через открытую дверь к финишу.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипты кнопки и финиша</h3>
|
||
<ScriptKind kind="object" on="каждую красную кнопку" />
|
||
<Code>{`// === Скрипт кнопки 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 });`}</Code>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('escape');
|
||
});`}</Code>
|
||
<p>
|
||
Кнопка при нажатии становится зелёной (видно, что нажата),
|
||
шлёт <code>game.broadcast('pressButton')</code> и больше
|
||
не срабатывает благодаря флажку <code>used</code>.
|
||
</p>
|
||
<Note>
|
||
Скрипт у всех трёх кнопок <b>одинаковый</b> — каждая просто
|
||
шлёт сообщение <code>'pressButton'</code>. Главный скрипт
|
||
сам считает, сколько кнопок нажато.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>нажми <kbd className="kbd">E</kbd> на видимых кнопках —
|
||
они становятся зелёными;</li>
|
||
<li>найди третью кнопку за ящиком;</li>
|
||
<li>после третьей дверь поднимается вверх;</li>
|
||
<li>выйди наружу на зелёный финиш — «Победа».</li>
|
||
</ul>
|
||
|
||
<Try>
|
||
спрячь все кнопки похитрее — например, в разных комнатах.
|
||
Добавь четвёртую кнопку. Сделай «ложную» кнопку, которая
|
||
сбрасывает счётчик.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 48 — «Мультиплеер: Салки»
|
||
// ════════════════════════════════════════════════════
|
||
'mp-tag': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Догонялки для нескольких игроков. Один игрок — водящий,
|
||
он догоняет остальных, а они убегают и прячутся за укрытиями.
|
||
Это <b>мультиплеерная</b> игра: чтобы играть с друзьями,
|
||
её нужно опубликовать с галочкой «Мультиплеер».
|
||
</p>
|
||
<Shot src="lesson48-result.png"
|
||
caption="Салки: водящий догоняет, остальные прячутся" />
|
||
|
||
<Note>
|
||
<b>Важно про мультиплеер.</b> Чтобы играть с друзьями,
|
||
опубликуй игру и поставь галочку <b>«Мультиплеер»</b> —
|
||
тогда несколько игроков смогут зайти в одну комнату.
|
||
Если открыть игру в одиночку, она работает как
|
||
<b> демо</b>: ты увидишь правила, но догонять будет некого.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Игроки комнаты</b> — <code>game.players</code>:
|
||
список всех, кто играет;</li>
|
||
<li><b>Общее состояние</b> — <code>game.room</code>: данные,
|
||
которые видят все игроки;</li>
|
||
<li><b>Вход и выход</b> — события
|
||
<code> onPlayerJoin</code> / <code>onPlayerLeave</code>;</li>
|
||
<li><b>Реакция на изменение</b> —
|
||
<code> game.room.onChange</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поле с укрытиями</h3>
|
||
<Step n="1">
|
||
Построй большую травяную площадку (примерно 28×28 блоков).
|
||
</Step>
|
||
<Step n="2">
|
||
Расставь по полю несколько кубов-укрытий (3.5×4×1.5,
|
||
серые) — за ними убегающие смогут прятаться.
|
||
</Step>
|
||
<Shot src="lesson48-scene.png"
|
||
caption="Поле для салок с укрытиями" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт ===
|
||
//
|
||
// Это МУЛЬТИПЛЕЕРНАЯ игра. Чтобы играть с друзьями, опубликуй её
|
||
// с галочкой «Мультиплеер» — тогда в комнату смогут зайти несколько
|
||
// игроков. В одиночку игра показывает только правила.
|
||
|
||
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);
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.players.count()</code> — сколько игроков
|
||
сейчас в комнате;</li>
|
||
<li><code>game.players.all()</code> — список всех игроков;
|
||
<code> me()</code> — это «я»;</li>
|
||
<li><code>game.room.set('tagger', ...)</code> — записываем
|
||
в общее состояние комнаты, кто водящий. Это видят
|
||
все игроки;</li>
|
||
<li><code>game.onPlayerJoin / onPlayerLeave</code> —
|
||
события входа и выхода игрока;</li>
|
||
<li><code>game.room.onChange('tagger', ...)</code> —
|
||
срабатывает у всех, когда водящий поменялся: каждый
|
||
узнаёт, он водящий или убегает.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Проверка</h3>
|
||
<ul>
|
||
<li>в одиночку игра покажет правила (демо-режим);</li>
|
||
<li>опубликуй игру с галочкой «Мультиплеер»;</li>
|
||
<li>пусть друзья зайдут по ссылке в твою комнату;</li>
|
||
<li>первый зашедший — водящий, остальные убегают.</li>
|
||
</ul>
|
||
<Note>
|
||
Разница простой и мультиплеерной игры: обычные данные
|
||
(<code>let score</code>) есть только у тебя, а
|
||
<code> game.room</code> — общие для всей комнаты. Поэтому
|
||
«кто водящий» хранят именно в <code>game.room</code>.
|
||
</Note>
|
||
|
||
<Try>
|
||
добавь надпись с именем водящего через
|
||
<code> game.ui.set</code>. Сделай так, чтобы при касании
|
||
водящего с убегающим они менялись ролями. Добавь таймер
|
||
раунда.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 49 — «Мультиплеер: Гонка»
|
||
// ════════════════════════════════════════════════════
|
||
'mp-race': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Гонка нескольких игроков по одной трассе. Кто первым
|
||
добежит до финиша — его имя записывается в общий счёт
|
||
комнаты, и все игроки видят победителя. Это снова
|
||
<b> мультиплеерная</b> игра.
|
||
</p>
|
||
<Shot src="lesson49-result.png"
|
||
caption="Мультиплеер-гонка: первый на финише — победитель" />
|
||
|
||
<Note>
|
||
<b>Важно про мультиплеер.</b> Чтобы соревноваться
|
||
с друзьями, опубликуй игру с галочкой
|
||
<b> «Мультиплеер»</b> — тогда несколько игроков попадут
|
||
в одну комнату и побегут вместе. В одиночку игра
|
||
открывается как <b>демо</b>.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Общий счёт комнаты</b> — <code>game.room</code>
|
||
хранит имя победителя для всех;</li>
|
||
<li><b>Узнать себя</b> — <code>game.players.me()</code>
|
||
и имя игрока;</li>
|
||
<li><b>Один победитель</b> — как не дать записать второго;</li>
|
||
<li><b>Обновление у всех</b> — <code>game.room.onChange</code>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Трасса</h3>
|
||
<Step n="1">
|
||
Построй длинную прямую дорожку из камня (около 70 блоков)
|
||
с бортиками по бокам.
|
||
</Step>
|
||
<Step n="2">
|
||
В конце поставь широкий зелёный куб «Финиш».
|
||
</Step>
|
||
<Step n="3">
|
||
Точку спавна поставь в начале трассы.
|
||
</Step>
|
||
<Shot src="lesson49-scene.png"
|
||
caption="Прямая гоночная трасса с финишем" />
|
||
|
||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт ===
|
||
//
|
||
// Мультиплеерная гонка. Чтобы соревноваться с друзьями — опубликуй
|
||
// игру с галочкой «Мультиплеер».
|
||
|
||
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);
|
||
}
|
||
});`}</Code>
|
||
<p>Разберём:</p>
|
||
<ul>
|
||
<li><code>game.room.get('winner')</code> — читаем общую
|
||
переменную комнаты: записан ли уже победитель;</li>
|
||
<li><code>refresh()</code> — рисует надпись со счётом:
|
||
число игроков и имя победителя;</li>
|
||
<li><code>game.onMessage('finish', ...)</code> — если
|
||
победителя ещё нет, игрок записывает <b>своё имя</b>
|
||
через <code>game.room.set('winner', myName)</code>;</li>
|
||
<li><code>if (!game.room.get('winner'))</code> — защита:
|
||
только <b>первый</b> добежавший станет победителем,
|
||
остальные увидят «кто-то был быстрее»;</li>
|
||
<li><code>game.room.onChange('winner', ...)</code> — как
|
||
только победитель записан, надпись обновляется
|
||
<b> у всех игроков сразу</b>.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
|
||
<ScriptKind kind="object" on="зелёный финиш" />
|
||
<Code>{`// === Скрипт финиша ===
|
||
game.self.onTouch(() => {
|
||
game.broadcast('finish');
|
||
});`}</Code>
|
||
<p>
|
||
Когда любой игрок касается финиша, скрипт шлёт сообщение
|
||
<code> game.broadcast('finish')</code> — а главный скрипт
|
||
ловит его и уже решает, первый этот игрок или нет.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<ul>
|
||
<li>в одиночку — демо: добежишь и станешь победителем сам;</li>
|
||
<li>опубликуй с галочкой «Мультиплеер»;</li>
|
||
<li>пусть друзья зайдут в комнату и побегут с тобой;</li>
|
||
<li>первый на финише — его имя в надписи у всех.</li>
|
||
</ul>
|
||
<Note>
|
||
Главная идея: <code>game.room</code> — это «общая доска»
|
||
комнаты. Один игрок пишет на неё имя победителя, и все
|
||
остальные тут же это видят.
|
||
</Note>
|
||
|
||
<Try>
|
||
добавь секундомер и записывай в комнату ещё и время
|
||
победителя. Сделай несколько кругов. Добавь места для
|
||
2-3 места, не только для первого.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// ИГРА 50 — «Своя игра»
|
||
// ════════════════════════════════════════════════════
|
||
'make-your-own': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что это за урок</h3>
|
||
<p>
|
||
Это последний урок — и он особенный. Здесь нет готовой
|
||
игры. Тебе даётся <b>пустая песочница</b>: ровная зелёная
|
||
площадка и один скрипт-приветствие. Твоя задача — придумать
|
||
и собрать <b>свою собственную игру</b> с нуля. Всё, что
|
||
нужно, ты уже знаешь из уроков 1-49.
|
||
</p>
|
||
<Shot src="lesson50-result.png"
|
||
caption="Пустая песочница — здесь рождается твоя игра" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Придумывать игру</b> — выбирать жанр и идею;</li>
|
||
<li><b>Планировать сцену</b> — что и где построить;</li>
|
||
<li><b>Ставить цель</b> — что игрок должен сделать,
|
||
чтобы выиграть;</li>
|
||
<li><b>Соединять механики</b> — брать кусочки из разных
|
||
уроков и складывать в свою игру.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Выбери жанр</h3>
|
||
<p>
|
||
Сначала реши, <b>какая</b> у тебя игра. Жанр — это «вид»
|
||
игры. Вот какие ты уже умеешь делать:
|
||
</p>
|
||
<ul>
|
||
<li><b>Паркур</b> — прыжки по платформам (уроки 2, 10, 41);</li>
|
||
<li><b>Гонка на время</b> — добежать быстрее (уроки 8, 32, 43);</li>
|
||
<li><b>Сбор предметов</b> — собрать всё на уровне (уроки 1, 6, 34);</li>
|
||
<li><b>Стрелялка</b> — отбиваться от врагов (уроки 15, 40, 45);</li>
|
||
<li><b>Квест</b> — головоломки и задания (уроки 12, 30, 42, 47);</li>
|
||
<li><b>Tower Defense</b> — защита базы (уроки 31, 44);</li>
|
||
<li><b>Мультиплеер</b> — игра с друзьями (уроки 48, 49).</li>
|
||
</ul>
|
||
<Note>
|
||
Не бойся смешивать! Лучшие игры — это сочетание: паркур
|
||
с монетками и врагом, квест с гонкой. Возьми один главный
|
||
жанр и добавь к нему пару механик из других.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 2. Спланируй сцену</h3>
|
||
<p>
|
||
Прежде чем строить — представь свою игру. Ответь себе
|
||
на вопросы:
|
||
</p>
|
||
<ul>
|
||
<li><b>Где начинает игрок?</b> — поставь точку спавна;</li>
|
||
<li><b>Где он будет ходить?</b> — построй пол, платформы,
|
||
дорогу из блоков;</li>
|
||
<li><b>Что мешает?</b> — шипы, ямы, враги, стены;</li>
|
||
<li><b>Что помогает?</b> — монетки, бусты, чекпоинты;</li>
|
||
<li><b>Где конец игры?</b> — финиш, сокровище или
|
||
нужный счёт.</li>
|
||
</ul>
|
||
<p>
|
||
Можно даже нарисовать план на бумаге — это помогает
|
||
не запутаться.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 3. Поставь цель</h3>
|
||
<p>
|
||
У каждой игры должна быть <b>понятная цель</b> — то, ради
|
||
чего игрок играет. Без цели игра скучная. Цель может быть
|
||
такой:
|
||
</p>
|
||
<ul>
|
||
<li>добраться до финиша;</li>
|
||
<li>собрать все предметы;</li>
|
||
<li>набрать нужное число очков;</li>
|
||
<li>продержаться какое-то время;</li>
|
||
<li>выполнить квест.</li>
|
||
</ul>
|
||
<p>
|
||
И обязательно покажи игроку, когда он <b>победил</b> —
|
||
надписью <code>game.ui.showText('Победа!', 5)</code>,
|
||
звуком <code>game.sound.play('win')</code> и конфетти.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Напиши скрипты</h3>
|
||
<p>
|
||
Сцена сама по себе не «живая» — её оживляют скрипты.
|
||
Начинай с <b>главного скрипта</b>: в нём заводи переменные
|
||
(счёт, флажок победы) и <b>лови сообщения</b> через
|
||
<code> game.onMessage('имя', fn)</code>. На объекты вешай
|
||
небольшие скрипты — они шлют сообщения главному через
|
||
<code> game.broadcast('имя')</code>. Так главный скрипт
|
||
узнаёт, что монетку собрали или кнопку нажали. Ты делал
|
||
так в каждом уроке.
|
||
</p>
|
||
<Note>
|
||
Каждый скрипт работает в своей «песочнице» — переменные
|
||
одного скрипта не видны другому. Поэтому скрипты общаются
|
||
сообщениями: один шлёт <code>game.broadcast('имя')</code>,
|
||
другой ловит <code>game.onMessage('имя', fn)</code>. Можно
|
||
передать данные: <code>game.broadcast('имя', {'{'} ... {'}'})</code>.
|
||
</Note>
|
||
<p>Базовый набор инструментов, который ты знаешь:</p>
|
||
<ul>
|
||
<li><code>game.self.onTouch</code> — реакция на касание;</li>
|
||
<li><code>game.self.onInteract</code> — реакция на
|
||
<kbd className="kbd">E</kbd>;</li>
|
||
<li><code>game.self.onClick</code> — реакция на клик;</li>
|
||
<li><code>game.broadcast</code> и <code>game.onMessage</code>
|
||
— связь между скриптами;</li>
|
||
<li><code>game.onTick</code> — каждый кадр;</li>
|
||
<li><code>game.after</code> и <code>game.every</code> —
|
||
таймеры;</li>
|
||
<li><code>game.tween</code> — плавное движение;</li>
|
||
<li><code>game.scene.spawnNpc</code> — враги и NPC;</li>
|
||
<li><code>game.ui.score</code> и
|
||
<code> game.ui.showText</code> — счёт и подсказки.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 5. Проверяй и улучшай</h3>
|
||
<p>
|
||
Не строй сразу всё. Делай по чуть-чуть и почаще нажимай
|
||
<kbd className="kbd">Запустить</kbd>: построил пол —
|
||
проверь, добавил врага — проверь. Так легче найти ошибку.
|
||
Если что-то не работает — открой <b>Консоль</b>, там видны
|
||
ошибки скриптов.
|
||
</p>
|
||
<Note>
|
||
Не существует «неправильной» игры. Если в неё весело
|
||
играть — она удалась. Начни с простого: маленький уровень,
|
||
одна цель. А потом добавляй новое: врага, монетки, второй
|
||
этаж. Большие игры всегда начинались с маленьких.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Поздравляем!</h3>
|
||
<p>
|
||
Ты прошёл все 50 уроков и теперь умеешь строить сцены,
|
||
писать скрипты, оживлять врагов, делать квесты, гонки
|
||
и даже мультиплеер. Это настоящие навыки создателя игр.
|
||
Впереди — только твоя фантазия. Придумывай, строй,
|
||
показывай друзьям. Удачи, и пусть твои игры будут
|
||
лучшими!
|
||
</p>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// РАЗБОР ИГР · Двор с табличкой (оригинал id=1991)
|
||
// ════════════════════════════════════════════════════
|
||
'guide-dvor': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Маленький уютный двор: зелёный газон, деревянный забор,
|
||
деревья и большая <b>3D-табличка</b> в центре. По табличке
|
||
можно нажать мышкой прямо в игре — и что-то произойдёт.
|
||
А ещё двор учит главному: как крутить камеру вокруг героя,
|
||
как в настоящем Roblox.
|
||
</p>
|
||
<Shot src="guide-dvor-scene.png"
|
||
caption="Двор в редакторе: газон в заборе, табличка и плитки" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Камера и мышь</b> — зажми <b>правую кнопку мыши</b> и
|
||
веди, чтобы осмотреться вокруг героя;</li>
|
||
<li><b>Зум</b> — колесо мыши приближает и отдаляет. Совсем
|
||
близко — игра сама переходит в вид «от первого лица»;</li>
|
||
<li><b>Shift-Lock</b> на клавише <kbd className="kbd">L</kbd> —
|
||
герой всегда смотрит туда же, куда камера;</li>
|
||
<li><b>Клик по 3D-табличке</b> — как сделать кнопку прямо
|
||
в игровом мире (<code>game.billboard.onClick</code>).</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Двор</h3>
|
||
<Step n="1">
|
||
Инструментом <kbd className="kbd">Блок</kbd> построй площадку
|
||
из травы примерно 10×10. Это газон.
|
||
</Step>
|
||
<Step n="2">
|
||
По краю поставь забор из блоков-брёвен в 2 блока высотой,
|
||
оставив спереди проход.
|
||
</Step>
|
||
<Step n="3">
|
||
Из палитры моделей добавь пару деревьев — для красоты.
|
||
На вкладке <b>Игра</b> поставь <b>точку спавна</b> в центре.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Табличка</h3>
|
||
<p>
|
||
Табличка — это особый примитив <b>«3D-табличка»</b>
|
||
(биллборд). У неё есть кнопка, на которую можно нажимать.
|
||
</p>
|
||
<Step n="1">
|
||
Выбери <kbd className="kbd">Примитив</kbd> →
|
||
категория <b>Геймплей</b> → <b>3D-табличка</b>. Поставь её
|
||
в центр двора.
|
||
</Step>
|
||
<Step n="2">
|
||
Выдели табличку. В инспекторе справа нажми
|
||
<b> «Редактировать табличку»</b> — откроется окно, где можно
|
||
задать текст, иконку, цвет и кнопку.
|
||
</Step>
|
||
<Step n="3">
|
||
Запомни <b>номер таблички</b> — кликни по ней в Иерархии,
|
||
он выглядит как <code>primitive:41</code> (число у тебя может
|
||
быть другое).
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт клика</h3>
|
||
<p>
|
||
Теперь сделаем, чтобы по нажатию на табличку менялся цвет неба.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ДВОР С ТАБЛИЧКОЙ — главный скрипт ===
|
||
|
||
// Подписываемся на клик по кнопке таблички.
|
||
// 'primitive:41' — номер твоей таблички, 'buy' — её кнопка.
|
||
game.billboard.onClick('primitive:41', 'buy', () => {
|
||
game.environment.setSkyColor('#88c0ff'); // небо стало голубым
|
||
game.log('По табличке нажали!');
|
||
});`}</Code>
|
||
<p>
|
||
<code>game.billboard.onClick(номер, кнопка, функция)</code> —
|
||
«когда нажмут на эту кнопку, выполни функцию». Внутри мы
|
||
меняем цвет неба командой
|
||
<code> game.environment.setSkyColor</code>.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<Shot src="guide-dvor-play.png"
|
||
caption="Двор в игре: герой от третьего лица, камера крутится мышкой" />
|
||
<Step n="1">
|
||
Нажми <kbd className="kbd">Играть</kbd>. Походи по двору
|
||
на <kbd className="kbd">W</kbd><kbd className="kbd">A</kbd><kbd className="kbd">S</kbd><kbd className="kbd">D</kbd>.
|
||
</Step>
|
||
<Step n="2">
|
||
Зажми <b>правую кнопку мыши</b> и веди — камера крутится
|
||
вокруг героя. Покрути колесо — приближается и отдаляется.
|
||
</Step>
|
||
<Step n="3">
|
||
Наведи курсор на кнопку таблички и кликни — небо поменяет цвет.
|
||
</Step>
|
||
<Note>
|
||
Камера и мышь — это фундамент почти любой игры. Разберёшься
|
||
здесь — дальше будет намного легче.
|
||
</Note>
|
||
<Try>
|
||
поставь рядом ещё две таблички с разными цветами неба и
|
||
сделай переключатель «утро — день — ночь» из трёх кнопок.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// РАЗБОР ИГР · Витрина GUI (оригинал id=1995)
|
||
// ════════════════════════════════════════════════════
|
||
'guide-vitrina': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Витрина магазина, как в играх-кликерах. 3D-мира почти нет —
|
||
весь экран это <b>интерфейс</b>: счётчик монет и яркие кнопки.
|
||
И все кнопки <b>живые</b>: пульсируют, крутятся, увеличиваются
|
||
при наведении и «вдавливаются» при нажатии.
|
||
</p>
|
||
<Shot src="guide-vitrina-scene.png"
|
||
caption="Витрина: счётчик монет, кнопки магазина, радужная «X2 ДЕНЕГ»" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>GUI-кнопки</b> с градиентом, обводкой текста и
|
||
скруглением;</li>
|
||
<li><b>Анимация-пресет</b> — кнопка пульсирует сама по себе
|
||
(свойство «Анимация: pulse»);</li>
|
||
<li><b>Твин</b> — плавное изменение: кнопка плавно крутится
|
||
с 0° до 360° (<code>game.gui.tween</code>);</li>
|
||
<li><b>Связь кнопок и счётчика</b> через сообщения
|
||
(<code>game.broadcast</code> и <code>game.onMessage</code>).</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Счётчик и кнопки</h3>
|
||
<Step n="1">
|
||
Поставь маленький пол и точку спавна (мир мы почти не видим).
|
||
</Step>
|
||
<Step n="2">
|
||
На вкладке <b>Интерфейс</b> добавь надпись-счётчик слева
|
||
сверху — это монеты.
|
||
</Step>
|
||
<Step n="3">
|
||
Добавь кнопку. В инспекторе задай ей <b>градиент фона</b>,
|
||
<b> обводку текста</b> и скругление углов. Размер задаётся
|
||
в процентах: ширина <code>18</code> — это 18% экрана.
|
||
</Step>
|
||
<Note>
|
||
Поля кнопок задаются <b>в процентах</b> от экрана, а не в
|
||
пикселях. Так интерфейс одинаково выглядит на любом мониторе.
|
||
Если поставить ширину 220 — кнопка растянется на весь экран!
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 2. Живые анимации</h3>
|
||
<Step n="1">
|
||
Выдели кнопку и в инспекторе выбери свойство
|
||
<b> «Анимация: pulse»</b> — в игре она начнёт пульсировать сама.
|
||
</Step>
|
||
<Step n="2">
|
||
Там же есть <b>hover</b> (что делать при наведении мышкой,
|
||
например увеличиться) и <b>active</b> (при нажатии — сжаться).
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 3. Скрипт кнопки «X2»</h3>
|
||
<p>
|
||
Повесим на кнопку «X2 денег» скрипт: при клике она
|
||
эффектно крутится и на 5 секунд удваивает награду.
|
||
</p>
|
||
<ScriptKind kind="object" on="кнопку «X2 денег»" />
|
||
<Code>{`// === Скрипт кнопки 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));
|
||
});`}</Code>
|
||
<p>
|
||
<code>game.gui.tween(объект, что-меняем, как-долго)</code> —
|
||
это и есть плавная анимация. Без неё кнопка прыгнула бы резко,
|
||
а с твином крутится гладко, как в дорогих играх.
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 4. Главный скрипт-счётчик</h3>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === Витрина 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; });`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 5. Проверка</h3>
|
||
<Step n="1">Нажми <kbd className="kbd">Играть</kbd>.</Step>
|
||
<Step n="2">Жми кнопки — счётчик монет растёт.</Step>
|
||
<Step n="3">Нажми «X2» — кнопка крутится, и 5 секунд монеты идут вдвое быстрее.</Step>
|
||
<Try>
|
||
сделай кнопку, которая при наведении мышкой меняет цвет
|
||
(свойство <b>hover</b>), а при клике плавно уезжает за край
|
||
экрана через твин по координате X.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// РАЗБОР ИГР · Тайна старого сундука (оригинал id=2037)
|
||
// ════════════════════════════════════════════════════
|
||
'guide-sunduk': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Маленькое приключение. Лесная поляна с каменными руинами,
|
||
светящийся сундук в центре и страж рядом. Игра показывает
|
||
<b> модальные сцены</b> — это когда мир затемняется, всё
|
||
замирает, и на экране появляется что-то важное: диалог,
|
||
выбор приза или большая надпись.
|
||
</p>
|
||
<Shot src="guide-sunduk-scene.png"
|
||
caption="Поляна с руинами: страж слева, светящийся сундук в центре" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Диалог</b> по фразам с кнопкой ▶
|
||
(<code>game.modal.dialog</code>);</li>
|
||
<li><b>Кат-сцена</b> — затемнить мир, оставить «прожектор»
|
||
на объекте, подлететь камерой (<code>game.modal.open</code>);</li>
|
||
<li><b>Вопрос Да/Нет</b> (<code>game.modal.confirmation</code>);</li>
|
||
<li><b>Лутбокс</b> — выбор приза из карточек
|
||
(<code>game.modal.lootbox</code>);</li>
|
||
<li>как <b>находить объект по имени</b> и следить за
|
||
расстоянием до него.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Поляна, сундук и страж</h3>
|
||
<Step n="1">
|
||
Построй каменную площадку (greystone) с обломками стен и
|
||
колоннами вокруг — это руины.
|
||
</Step>
|
||
<Step n="2">
|
||
В центр поставь сундук (можно собрать из примитива-куба) и
|
||
в инспекторе дай ему <b>имя «chest»</b>.
|
||
</Step>
|
||
<Step n="3">
|
||
Слева поставь стража (из примитивов: цилиндр-тело, сфера-голова,
|
||
конус-шлем) с <b>именем «guard»</b>.
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Диалог со стражем</h3>
|
||
<p>
|
||
В обычном Roblox такую сцену собирают из 5-6 кусков вручную.
|
||
У нас — одна команда. Главный скрипт следит за расстоянием
|
||
до стража и запускает диалог.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ТАЙНА СУНДУКА — главный скрипт ===
|
||
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('Иди к сундуку!'));
|
||
}
|
||
}
|
||
});`}</Code>
|
||
<Shot src="guide-sunduk-play.png"
|
||
caption="Диалог в игре: фраза стража внизу и кнопка ▶ для продолжения" />
|
||
<Note>
|
||
Имя объекта (например «chest») ищи не сразу при старте, а
|
||
внутри <code>game.onTick</code> — сцена «появляется» не
|
||
мгновенно, и в первую секунду объект ещё не найден.
|
||
</Note>
|
||
|
||
<h3 className="lessonH">Шаг 3. Кат-сцена сундука</h3>
|
||
<p>
|
||
Подошёл к сундуку — затемняем мир, но сундук оставляем ярким
|
||
(вокруг него прожектор), камера сама подлетает.
|
||
</p>
|
||
<Code>{`// (продолжение 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' },
|
||
]},
|
||
});
|
||
}
|
||
}`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 4. Выбор приза и победа</h3>
|
||
<p>
|
||
Готовые сцены, которые вызываются одной строкой:
|
||
</p>
|
||
<ul>
|
||
<li><code>game.modal.confirmation('Открыть?', 'текст', наДа, наНет)</code> — вопрос Да/Нет;</li>
|
||
<li><code>game.modal.lootbox([призы], выбор)</code> — карточки призов;</li>
|
||
<li><code>game.modal.bossIntro(имя, hp, [враги])</code> — заставка перед боссом.</li>
|
||
</ul>
|
||
<Code>{`// Лутбокс — четыре приза, игрок выбирает один
|
||
game.modal.lootbox([
|
||
{ name: 'Меч зари', icon: '⚔', color: '#c0392b' },
|
||
{ name: 'Щит луны', icon: '🛡', color: '#2c5fb3' },
|
||
{ name: 'Кошель злата', icon: '💰', color: '#b8860b' },
|
||
{ name: 'Перо феникса', icon: '🔥', color: '#8e44ad' },
|
||
], (item) => {
|
||
game.log('Игрок выбрал: ' + item.name);
|
||
});`}</Code>
|
||
|
||
<h3 className="lessonH">Шаг 5. Проверка</h3>
|
||
<Step n="1">Нажми <kbd className="kbd">Играть</kbd> и подойди к стражу — пойдёт диалог.</Step>
|
||
<Step n="2">Иди к сундуку — мир затемнится, камера подлетит к нему.</Step>
|
||
<Step n="3">Ответь «Да», выбери приз — увидишь финальную надпись.</Step>
|
||
<Note>
|
||
Кнопка диалога на последней фразе сама превращается из ▶
|
||
в галочку ✓ — это значит «закрыть диалог».
|
||
</Note>
|
||
<Try>
|
||
добавь второго стража с другим квестом и сделай так, чтобы
|
||
сундук открывался только после разговора с обоими.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
// ════════════════════════════════════════════════════
|
||
// РАЗБОР ИГР · Парк животных (оригинал id=2046)
|
||
// ════════════════════════════════════════════════════
|
||
'guide-zoo': {
|
||
body: (
|
||
<>
|
||
<h3 className="lessonH">Что получится</h3>
|
||
<p>
|
||
Самая весёлая игра! Ты начинаешь её в виде... <b>пончика</b>.
|
||
По парку стоят таблички: нажми на кнопку таблички — и твой
|
||
герой <b>превращается</b> в бургер, болид, пришельца, рыбу
|
||
или человечка. А по клавише <kbd className="kbd">B</kbd>
|
||
открывается <b>магазин скинов</b>.
|
||
</p>
|
||
<Shot src="guide-zoo-scene.png"
|
||
caption="Парк: 6 табличек со скинами и баннер «Примерочная скинов»" />
|
||
|
||
<h3 className="lessonH">Чему научишься</h3>
|
||
<ul>
|
||
<li><b>Кастомный скин</b> — герой может быть любой 3D-моделью,
|
||
не только человечком (<code>game.player.setSkin</code>);</li>
|
||
<li><b>Смена скина в игре</b> — без перезапуска, прямо на ходу;</li>
|
||
<li><b>Магазин скинов</b> — встроенный, открывается клавишей
|
||
<kbd className="kbd">B</kbd>;</li>
|
||
<li>снова <b>3D-таблички</b> — те же, что в игре «Двор», но
|
||
теперь их кнопка меняет скин.</li>
|
||
</ul>
|
||
|
||
<h3 className="lessonH">Шаг 1. Парк и стартовый скин</h3>
|
||
<Step n="1">
|
||
Построй полянку с деревьями и поставь точку спавна.
|
||
</Step>
|
||
<Step n="2">
|
||
Нажми кнопку <b>«Скины»</b> в шапке редактора. Выбери
|
||
<b> стартовый скин</b> (например пончик), включи галочку
|
||
<b> «Магазин скинов»</b> и задай стартовые рублики (например 200).
|
||
</Step>
|
||
<Step n="3">
|
||
Поставь несколько 3D-табличек (как в игре «Двор») — по одной
|
||
на каждый скин. Запомни их номера (<code>primitive:10</code> и т.д.).
|
||
</Step>
|
||
|
||
<h3 className="lessonH">Шаг 2. Скрипт превращений</h3>
|
||
<p>
|
||
Главный скрипт подписывается на клик по каждой табличке и
|
||
меняет скин одной командой.
|
||
</p>
|
||
<ScriptKind kind="global" />
|
||
<Code>{`// === ПАРК ЖИВОТНЫХ — главный скрипт ===
|
||
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);
|
||
});`}</Code>
|
||
<p>
|
||
<code>game.player.setSkin('burger')</code> — одна строчка
|
||
меняет всё тело героя на новую модель. Имена скинов
|
||
(burger, car-race, alien…) видны в окне «Скины».
|
||
</p>
|
||
|
||
<h3 className="lessonH">Шаг 3. Магазин скинов</h3>
|
||
<p>
|
||
Магазин уже встроен — мы включили его галочкой в окне «Скины».
|
||
В игре он открывается клавишей <kbd className="kbd">B</kbd>.
|
||
Командами скинами тоже можно управлять:
|
||
</p>
|
||
<Code>{`game.player.unlockSkin('alien'); // открыть скин игроку
|
||
game.player.openSkinShop(); // открыть магазин из кода
|
||
game.player.setSkinCoins(500); // задать баланс рубликов`}</Code>
|
||
<Shot src="guide-zoo-play.png"
|
||
caption="Старт игры: твой герой — пончик! Он покачивается на ходу" />
|
||
|
||
<h3 className="lessonH">Шаг 4. Проверка</h3>
|
||
<Step n="1">Нажми <kbd className="kbd">Играть</kbd> — ты появишься пончиком.</Step>
|
||
<Step n="2">Походи — пончик смешно покачивается на ходу.</Step>
|
||
<Step n="3">Нажми на кнопку таблички — превратишься в другого героя.</Step>
|
||
<Step n="4">Нажми <kbd className="kbd">B</kbd> — откроется магазин скинов.</Step>
|
||
<Note>
|
||
Скины бывают двух видов: <b>человечки</b> (умеют махать,
|
||
прыгать, танцевать) и <b>модели</b> (пончик, машинка — они
|
||
просто покачиваются). Свою модель <code>.glb</code> тоже можно
|
||
загрузить в окне «Скины».
|
||
</Note>
|
||
<Try>
|
||
сделай скин платным: дай игроку 100 рубликов, поставь скину
|
||
цену 50 в магазине и проверь — хватит ли денег его купить.
|
||
</Try>
|
||
</>
|
||
),
|
||
},
|
||
|
||
};
|
||
|
||
/** Есть ли готовый текст урока для игры с таким id. */
|
||
export function hasLesson(id) {
|
||
return !!LESSONS[id];
|
||
}
|