# Туториал: как добавить новое scripting API Скрипты пользователей в Рублоксе работают через **песочницу `game.*`**. Все вызовы вида `game.player.damage(10)`, `game.scene.spawn(...)`, `game.ui.set(...)` — это методы которые ты можешь расширять. Этот туториал — как добавить **новый метод** в `game.*`. На примере: добавим `game.player.jump()` чтобы скрипт мог программно прыгнуть. ## Архитектура (важно понять до кода) ``` ┌──────────────────────────────────────────────────────┐ │ Скрипт юзера (его JS-код) │ │ game.player.jump(); │ └────────────────────┬─────────────────────────────────┘ │ postMessage({cmd:'player.jump'}) ▼ ┌──────────────────────────────────────────────────────┐ │ ScriptSandboxWorker.js (Web Worker, изолирован) │ │ Прокси-объект game.* — обёртки которые шлют │ │ postMessage в основной поток │ └────────────────────┬─────────────────────────────────┘ │ Worker.onmessage ▼ ┌──────────────────────────────────────────────────────┐ │ GameRuntime.js (главный поток) │ │ Обработчик _handleScriptCommand(cmd, payload): │ │ if (cmd === 'player.jump') { ... } │ │ Здесь — реальный вызов BabylonScene / PlayerCtrl │ └────────────────────┬─────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────┐ │ PlayerController + BabylonScene │ │ Изменение velocity, проигрывание анимации, и т.д. │ └──────────────────────────────────────────────────────┘ ``` **Почему так сложно:** скрипты юзеров — недоверенный код. Они в Worker'е без доступа к DOM, `window`, `fetch`. Единственный канал коммуникации — `postMessage`. Это безопасность. ## Шаг 1. Реализуй фичу в движке Открой `src/editor/engine/PlayerController.js`. Найди где обрабатывается прыжок (по клавише Space): ```javascript // Где-то в PlayerController.js _handleInput() { if (this._keys.space && this._isGrounded) { this._jump(); } } _jump() { this._velocity.y = this.jumpForce; this._isGrounded = false; } ``` Если у тебя есть метод `_jump()` — отлично. Если нет — добавь его на основе того кода что обрабатывает Space. Сделай его **публичным** (без подчёркивания): ```javascript jump() { if (!this._isGrounded) return; // нельзя прыгнуть в воздухе this._velocity.y = this.jumpForce; this._isGrounded = false; // опционально: проиграть звук this.scene3d.audio?.play('jump'); } ``` ## Шаг 2. Добавь обработчик в GameRuntime Открой `src/editor/engine/GameRuntime.js`. Найди функцию `_handleScriptCommand(cmd, payload)` — это длинный switch по `cmd`. Добавь свой блок (рядом с другими `player.*`): ```javascript if (cmd === 'player.jump') { try { this.playerCtrl?.jump(); } catch (e) { console.warn('[script] player.jump failed:', e); } return; } ``` Соглашение: - `cmd` строкой через точку: `.` - Всё оборачивай в `try/catch` — скрипт юзера не должен крашить движок - Если что-то возвращаешь — отвечай через `this._postBack(msgId, result)` ## Шаг 3. Добавь прокси в ScriptSandboxWorker Открой `src/editor/engine/ScriptSandboxWorker.js`. Найди объект `game.player`: ```javascript const game = { player: { damage(amount) { postCmd('player.damage', { amount }); }, heal(amount) { postCmd('player.heal', { amount }); }, kill() { postCmd('player.kill'); }, // ... }, }; ``` Добавь свой метод: ```javascript player: { damage(amount) { postCmd('player.damage', { amount }); }, heal(amount) { postCmd('player.heal', { amount }); }, kill() { postCmd('player.kill'); }, jump() { postCmd('player.jump'); }, // ← новое }, ``` Готово — скрипт юзера теперь может звать `game.player.jump()`. ## Шаг 4. Добавь TypeScript-декларацию (для Monaco autocomplete) Открой `src/editor/engine/types/player.d.ts`. Найди где описаны другие методы игрока: ```typescript declare namespace game.player { /** Снять HP игроку. */ function damage(amount: number): void; /** Восстановить HP. */ function heal(amount: number): void; // ... } ``` Добавь свой метод **со строгой типизацией и JSDoc-комментарием**: ```typescript /** * Заставить игрока прыгнуть программно (как нажатие Space). * Игнорируется если игрок в воздухе. * * @example * game.player.jump(); // прыжок */ function jump(): void; ``` JSDoc-комментарий важен — Monaco показывает его как подсказку когда юзер пишет `game.player.jump(`. ## Шаг 5. Пересобери Monaco-бандл деклараций `.d.ts` файлы склеиваются в один `bundle.js`, который Monaco грузит при старте: ```bash cd src/editor/engine/types python _build_bundle.py ``` Скрипт пройдёт по всем `*.d.ts`, склеит, обновит `bundle.js`. **Закоммить и `.d.ts` и `bundle.js`** — иначе у других контрибьюторов автокомплит будет показывать твою новую функцию, а в runtime получит `undefined`. ## Шаг 6. Протестируй вручную 1. Запусти dev-сервер: `npm run dev` 2. Открой студию: http://localhost:5174 3. Создай новый проект (или открой существующий) 4. Открой панель «Скрипты», создай новый скрипт с кодом: ```javascript game.onKey('e', () => { game.player.jump(); }); ``` 5. Жми Play. В игре нажми `E` — игрок должен прыгнуть. **Проверки на отказ:** - Не падает если игрок в воздухе (просто игнорится) - Не крашит движок если PlayerController ещё не создан - Не крашит если скрипт юзера зовёт `jump()` 100 раз/сек ## Шаг 7. Запиши в [CHANGELOG.md](../CHANGELOG.md) В секции `[Unreleased]`: ```markdown ## [Unreleased] ### Added - `game.player.jump()` — программный прыжок игрока ([#XX](https://git.rublox.pro/rublox/studio/pulls/XX)) ``` ## Шаг 8. PR Открой PR с описанием: ```markdown ## Что сделано Новое scripting API: `game.player.jump()` — программный прыжок игрока. ## Зачем Юзеры часто хотят сделать «авто-прыжок» в определённой зоне (платформер, GD-уровни). Раньше нужно было хак через эмуляцию нажатия Space. ## Как протестировать 1. Создай проект, добавь скрипт: ```js game.onKey('e', () => game.player.jump()); ``` 2. Запусти, нажми E в игре. 3. Игрок прыгает. ## Файлы - `src/editor/engine/PlayerController.js` — публичный метод `jump()` - `src/editor/engine/GameRuntime.js` — обработчик `cmd === 'player.jump'` - `src/editor/engine/ScriptSandboxWorker.js` — прокси `game.player.jump` - `src/editor/engine/types/player.d.ts` — TypeScript-декларация - `src/editor/engine/types/bundle.js` — пересобран - `CHANGELOG.md` — запись в Unreleased ``` ## Best practices ### ✅ Делай - **Try/catch на всё в `GameRuntime`** — скрипт юзера не должен крашить - **Throttle/debounce на UI-обновления** — `game.ui.set` 60 раз/сек убьёт FPS - **JSDoc с `@example`** — Monaco покажет пример - **Имя через точку**: `namespace.action` (`player.jump`, `scene.spawn`, `ui.set`) - **Возвращай примитивы**: number, string, boolean. Объекты — только простые `{x,y,z}` ### ❌ Не делай - **Не возвращай Babylon-объекты в Worker** — они не сериализуются через postMessage - **Не создавай новые Worker'ы внутри скрипта** — у нас один Worker на скрипт - **Не давай доступ к `fetch`, `XMLHttpRequest`, `WebSocket`** — это XSS-вектор - **Не позволяй вечный цикл** в синхронной обработке `_handleScriptCommand` — повесит главный поток - **Не добавляй deprecated API в `.d.ts`** — пусть устаревшие методы пропадут с автокомплита ## Что почитать дальше - [src/editor/engine/README.md](../src/editor/engine/README.md) — обзор архитектуры движка - [src/editor/engine/types/README.md](../src/editor/engine/types/README.md) — система типов для скриптов - `ScriptSandboxWorker.js` — изучи как сделаны существующие 100+ методов ## Вопросы Канал `#разработка` на https://team.rublox.pro