# Як захистити вихідний код гри на JavaScript: розбір мультиплеєрного проєкту

Захистіть вихідний код гри на JavaScript обфускацією клієнтської збірки. BIGBOARD.GAMES випускає мультиплеєрні ігри під захистом AfterPack без просідань FPS.

Source: https://www.afterpack.dev/uk/blog/protect-javascript-game-source-code

Published: 2026-10-08 · Author: Nikita Savchenko · Tags: guide, security, games

Щоб захистити вихідний код гри на JavaScript, обфускуйте клієнтську збірку, не пропускайте через обфускатор сторонні рушії й таблиці рядків, а секрети та кожну перевірку, яку може виконати сервер, тримайте на сервері. AfterPack ([afterpack.dev](https://www.afterpack.dev/)), обфускатор JavaScript, з кожним релізом перетворює зібрану гру на структурно іншу програму. Його рушій Free працює локально через [`npx afterpack`](https://www.afterpack.dev/docs/cli) або плагін для [Vite](https://www.afterpack.dev/docs/frameworks/vite), [webpack](https://www.afterpack.dev/docs/frameworks/webpack) чи [Rollup](https://www.afterpack.dev/docs/frameworks/rollup). [BIGBOARD.GAMES](https://bigboard.games/uk/), мультиплеєрна peer-to-peer гра, виходить із ним і тримає ту частоту кадрів, яку дозволяє браузер.

Я запустив [BIGBOARD.GAMES](https://bigboard.games/uk/) 6 жовтня. Це набір безкоштовних браузерних ігор для 2–4 телефонів і планшетів, зсунутих докупи на столі: кожен екран стає частиною одного ігрового поля, а в [«Настільному хокеї» (Seam Hockey)](https://bigboard.games/uk/games/seam-hockey) шайба ковзає через стик з одного пристрою на наступний. На старті були «Настільний хокей», [«Через край» (Spillover)](https://bigboard.games/uk/games/spillover), [«Бий крота» (Whack-a-Mole)](https://bigboard.games/uk/games/whack-a-mole) і [«Острови» (Islands)](https://bigboard.games/uk/games/islands). Гра йде напряму між пристроями через [WebRTC](https://en.wikipedia.org/wiki/WebRTC), без ігрового сервера посередині; [як влаштована сама гра](https://nikitaeverywhere.com/posts/bigboard-games/), я розповів у своєму блозі. AfterPack стояв у `vite.config.ts` уже в першому коміті репозиторію, тож без захисту гра не виходила жодного разу. Далі — скільки це коштувало грі в кадрах, байтах і часі збірки, і кожне число взято з реальної збірки.

Саме в іграх розробники найбільше бояться, що [обфускація](https://en.wikipedia.org/wiki/Obfuscation_(software)) зʼїсть частоту кадрів, і саме в іграх код, що дістається гравцеві, читають першим, бо правила, фізика й мережевий протокол працюють на його пристрої. ШІ зробив таке читання дешевим: агент перетворив власні демо двох популярних обфускаторів [назад на чистий вихідний код за 10 і 20 хвилин](https://www.afterpack.dev/blog/ai-deobfuscates-javascript). [Ті самі інструменти](https://claude.com/product/claude-code), з якими я створив BIGBOARD.GAMES приблизно за десять днів, дозволяють комусь іншому так само швидко її прочитати.

## Що видає код гри на JavaScript?

Усе. Правила, фізика, підрахунок очок і мережевий протокол, який мав би відтворити чит, лежать у бандлі, а [мініфікація](https://developer.mozilla.org/en-US/docs/Glossary/Minification) лише скорочує локальні імена: рядки, константи й структура лишаються читабельними, що рядок за рядком показує [цей розбір мініфікованого бандла](https://www.afterpack.dev/blog/protect-javascript-source-code#what-does-minified-javascript-expose).

У [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) грі клієнт заодно й суддя. У [«Настільному хокеї»](https://bigboard.games/uk/games/seam-hockey) шайбу симулює той пристрій, на якому вона зараз, і транслює її позицію іншим, а на стику керування шайбою переходить до наступного пристрою. [«Через край»](https://bigboard.games/uk/games/spillover) і [«Зграйка» (Shoal)](https://bigboard.games/uk/games/shoal) працюють у [детермінованому lockstep](https://gafferongames.com/post/deterministic_lockstep/): кожен пристрій проганяє ту саму симуляцію з тих самих вхідних даних і мусить прийти до того самого стану. [Cloudflare Worker](https://developers.cloudflare.com/workers/) із двома [Durable Objects](https://developers.cloudflare.com/durable-objects/) лише створює стіл, закріплює місця за гравцями й записує результати.

| Що отримує кожен гравець | Що дає його прочитання |
| --- | --- |
| Фізика шайби й передача керування на стиках | Де вирішується, чи був гол, і на якому пристрої |
| Lockstep-симуляції та їхня детермінована тригонометрія | Точний стан, на якому мають зійтися всі пристрої |
| Кодек повідомлень у [каналах даних WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel) | Протокол, який мав би відтворити модифікований клієнт |
| Калібрування екранів у реальних міліметрах (профілі пристроїв або [банківська картка](https://en.wikipedia.org/wiki/ISO/IEC_7810), прикладена до скла) | Те, що найбільше знадобилося б клону і що найважче відтворити |

## Чи сповільнює обфускація JavaScript гру?

У BIGBOARD.GAMES цього не видно. На стандартному [пресеті `light`](https://www.afterpack.dev/docs/presets#the-default-is-light) цикл кадрів «Настільного хокею» працює з тією частотою, яку браузер дає [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame): від 60 до 144 FPS на [OnePlus Pad 3](https://www.oneplus.com/global/oneplus-pad-3) і 60 FPS на iPad, де будь-який браузер — це [WebKit](https://webkit.org/), а WebKit тримає сторінки біля 60 FPS, якщо гравець не вимкне прапорець Safari «Prefer Page Rendering Updates near 60fps». Діагностика гри зчитує цю частоту з тієї самої обфускованої збірки, яку отримують гравці.

Частота кадрів — груба міра, тож я виміряв ще й сам код симуляції: ті самі функції, що є в іграх, забандлені через [esbuild](https://esbuild.github.io/), без обфускації та обфусковані на `light` рушієм, з яким запускався BIGBOARD (0.2.1), і поточним (0.2.3), у [V8](https://v8.dev/) і в [JavaScriptCore](https://docs.webkit.org/Deep%20Dive/JSC/JavaScriptCore.html).

| Код гри (одиниця) | Середовище | Без обфускації | Рушій 0.2.1 | Рушій 0.2.3 |
| --- | --- | ---: | ---: | ---: |
| Детермінований `sinCos` (нс на виклик) | V8 | 44,2 | 104,2 (2,36x) | 44,1 (1,00x) |
| Детермінований `sinCos` (нс на виклик) | JavaScriptCore | 9,5 | 57,4 (6,04x) | 20,6 (2,17x) |
| Симуляція «Зграйки» (мкс на тік) | V8 | 26,5 | 58,2 (2,20x) | 49,7 (1,88x) |
| Симуляція «Зграйки» (мкс на тік) | JavaScriptCore | 21,7 | 36,9 (1,71x) | 26,7 (1,23x) |
| Фізика шайби в «Настільному хокеї» (мкс на крок) | V8 | 0,52 | 1,01 (1,94x) | 0,78 (1,50x) |
| Фізика шайби в «Настільному хокеї» (мкс на крок) | JavaScriptCore | 0,39 | 0,66 (1,69x) | 0,64 (1,63x) |

Виміряно 2026-10-08 на Apple M2 Max у [Node.js](https://nodejs.org/) 24.21.0 (V8) і WebKit 26.6 з [Playwright](https://playwright.dev/) (JavaScriptCore): медіана 60–120 замірів у 4–8 нових процесах на кожен варіант, із тим самим seed, що й у випущеній збірці. У V8 один із восьми процесів «Зграйки» на рушії 0.2.3 стабілізувався приблизно на 67 мкс замість 50, залежно від того, як V8 вирішив оптимізувати саме цей запуск.

Тож обфускований код гри справді повільніший: на поточному рушії від 1,0x до 2,2x відносно звичайного коду. А частота кадрів однаково не змінюється, бо симуляція займає крихітну частку кадру. Найповільніший випадок у таблиці, [«Зграйка»](https://bigboard.games/uk/games/shoal) у V8 на рушії 0.2.1, витрачає 58,2 мкс на тік: це 0,35% кадру тривалістю 16,7 мс за частоти 60 Гц і 0,84% кадру тривалістю 6,9 мс за частоти 144 Гц. Планшет повільніший за M2 Max, але щоб цей тік заповнив кадр за частоти 144 Гц, процесор мав би бути більш ніж у 100 разів повільнішим.

[`light`](https://www.afterpack.dev/docs/presets#the-default-is-light) перейменовує ідентифікатори, пропускає рядкові літерали через декодер під час виконання й переписує синтаксис, але не додає структурних шарів. Починаючи з рушія 0.2.3, він ще й лишає цикли циклами: попередні версії рушія переписували їх на допоміжні функції з колбеками, що в гарячих циклах коштувало 2–5x ([журнал змін](https://www.afterpack.dev/changelog)). Імена властивостей і глобальні імена досі проходять через закодовані константи, а це [дещо коштує в дуже гарячому коді](https://www.afterpack.dev/docs/presets#the-default-is-light). [Пресети вище](https://www.afterpack.dev/docs/presets#the-ladder), від `medium` до `extreme`, додають структурну роботу на кожному токені. Якщо вони потрібні грі, націльте їх на найцінніший код, як-от мережевий, через [директиву Pro](https://www.afterpack.dev/docs/directives), а цикл кадрів лишіть на `light`.

## Чи ламає обфускація детермінований мультиплеєр?

Не повинна, і в BIGBOARD.GAMES не ламає: lockstep-іграм потрібен побітово однаковий стан на кожному пристрої, зокрема між JavaScriptCore у Safari та V8 у Chrome, і обфускована збірка його забезпечує.

Ця планка вища, ніж «гра все ще запускається». [`Math.sin`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin) не гарантує однакових бітів у всіх рушіях ([специфікація](https://tc39.es/ecma262/#sec-math.sin) називає його результат implementation-approximated, тобто точність лишається на розсуд реалізації), тому [«Через край»](https://bigboard.games/uk/games/spillover) поєднує [детерміновану збірку](https://rapier.rs/docs/user_guides/javascript/determinism) фізичного рушія [Rapier](https://rapier.rs/) із власним `sinCos`, що відхиляється від `Math.sin` не більше ніж на 5e-14. Цей `sinCos` і симуляції, які його викликають, обфусковані так само, як решта коду гри. У кожному заміряному прогоні вище я хешував повний стан симуляції, кожне число з рухомою комою за його точними бітами, і кожен обфускований варіант збігся зі звичайним і у V8, і в JavaScriptCore. Стан «Зграйки» та результати `sinCos` збіглися ще й між двома рушіями, а саме на цій властивості тримається lockstep. Щоночі набір тестів Playwright на кількох пристроях грає в ігри на staging, який віддає ту саму обфусковану збірку, що й гравцям. Серед тестів є матч у [«Зграйку»](https://bigboard.games/uk/games/shoal) на трьох пристроях: один гравець вилітає посеред гри, а двох інших перевіряють на розсинхронізацію.

Що обфускований результат зберігає незмінним і де він у кількох місцях навмисно відрізняється, описано на сторінці [семантичного контракту](https://www.afterpack.dev/docs/semantic-contract).

## Як обфускувати збірку гри на Vite

Встановіть [`@afterpack/vite`](https://www.afterpack.dev/docs/frameworks/vite), додайте `afterpackVite()` у `plugins` і виключіть чанки, які не ваші. `vite dev` лишається без змін, а `vite build` видає обфусковані чанки.

```bash
npm install -D @afterpack/vite
```

Конфіг нижче взято з BIGBOARD і скорочено до двох винятків. Я зібрав його в чистому проєкті 2026-10-08 з [Vite](https://vite.dev/) 8.3.3, `@afterpack/vite` 0.2.2 (рушій 0.2.3), пакетом Rapier [`@dimforge/rapier2d-deterministic-compat`](https://www.npmjs.com/package/@dimforge/rapier2d-deterministic-compat) 0.21.0 і Node.js 24.21.0. Квитанція позначила чанки Rapier та i18n як незмінені, а чанк гри як обфускований, і дві збірки одного коміту вийшли побайтово однаковими.

```ts
// vite.config.ts (Vite 8)
import { afterpackVite } from "@afterpack/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    afterpackVite({
      // One program per commit; the same commit and input give the same bytes.
      seed: "git",
      // Output files to leave as they are.
      paths: { exclude: ["**/rapier-*.js", "**/i18n-*.js"] },
    }),
  ],
  build: {
    rolldownOptions: {
      output: {
        codeSplitting: {
          groups: [
            // Give each excluded part its own chunk, or Vite merges it into one of yours.
            { name: "rapier", test: /[\\/]node_modules[\\/]@dimforge[\\/]/ },
            { name: "i18n", test: /[\\/]locales[\\/]/ },
          ],
        },
      },
    },
  },
});
```

Глоби в [`paths.exclude`](https://www.afterpack.dev/docs/config#paths-exclude) зіставляються з іменами вихідних файлів збірки, тож розбиття на чанки важить не менше, ніж самі глоби. Якщо пустити це на самоплив, Vite може злити бібліотеку в чанк, де лежить і ваш код, і тоді цей чанк обфускується цілком. Vite 8 розбиває чанки через [`codeSplitting`](https://rolldown.rs/reference/OutputOptions.codeSplitting) з Rolldown; у Vite 7 і старіших те саме розбиття робиться через [`manualChunks`](https://rollupjs.org/configuration-options/#output-manualchunks) з Rollup.

З 140 JavaScript-файлів BIGBOARD.GAMES лишає без обфускації 21. Які це чанки і чому, за вимірами на рушії 0.2.1:

| Чанк | Що це | Чому без обфускації |
| --- | --- | --- |
| `rapier-*.js` | Фізика [Rapier](https://rapier.rs/), детермінована збірка | Сторонній код: 3,4 МБ обвʼязки [WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly), де немає нічого нашого |
| `analytics-posthog-*.js`, `openapi-fetch-*.js`, `qr-*.js` | Аналітика [PostHog](https://posthog.com/), REST-клієнт [openapi-fetch](https://openapi-ts.dev/openapi-fetch/), QR-кодувальник | Сторонній код. Після обфускації один лише QR-кодувальник виріс утричі, приблизно до 25 КБ у gzip |
| `i18n-*.js` | Рядки англійською, українською та іспанською | Після обфускації звичайні рядки виросли приблизно в 10 разів, а секрету в жодному з них немає |
| `brand-marks-*.js`, `brand-glyphs-*.js` | Контури логотипів і SVG-розмітка | Дані контурів поводяться як рядки: після обфускації вхідний чанк виріс удесятеро |

`paths.exclude` доступний у [Free](https://www.afterpack.dev/docs/tiers). Щоб пропустити одну функцію всередині файлу, а не весь файл, потрібна [директива Pro](https://www.afterpack.dev/docs/directives); у збірці Free директива спричиняє помилку.

## Наскільки обфускація збільшує розмір гри?

Приблизно 1,65x у gzip на першому завантаженні з поточним рушієм і 2,2x з тим, на якому запускався BIGBOARD. `/play`, сторінка, яку відкривають гравці, завантажує 20 чанків:

| Що віддається, gzip | Без обфускації | Рушій 0.2.1 | Рушій 0.2.3 |
| --- | ---: | ---: | ---: |
| Перше завантаження `/play` | 107,9 КБ | 239,3 КБ (2,22x) | 178,5 КБ (1,65x) |
| Чанк [«Настільного хокею»](https://bigboard.games/uk/games/seam-hockey) | 20,5 КБ | 36,8 КБ (1,80x) | 29,5 КБ (1,44x) |
| Чанк гри [«Через край»](https://bigboard.games/uk/games/spillover) | 34,0 КБ | 59,6 КБ (1,75x) | 47,7 КБ (1,40x) |

Виміряно 2026-10-08 на `light`; обфусковані стовпці — медіана 7 збірок. Оновлення не вимагало нічого, крім зміни версії: той самий конфіг, ті самі винятки, а всі перевірки, `afterpack verify` і end-to-end прогін на кількох пристроях пройшли на новому рушії. Це чесна ціна обфускації: вона додає байти, і грі, яка рахує кілобайти, доводиться закладати їх у бюджет. Час збірки обходиться дешевше: уся збірка, тобто застосунок, лендинг і адмінка, займає 1,5 с без обфускації та 3,6 с з нею на M2 Max, на будь-якому з двох рушіїв. Власні виміри рушія є на [сторінці продуктивності](https://www.afterpack.dev/docs/performance) AfterPack.

Бюджет розміру дав один урок, який варто перейняти. Збірка BIGBOARD вписує час збірки у вхідний чанк, тож кожна збірка перейменовує і його, і кожен чанк, що його імпортує, а будь-який змінений чанк виходить з AfterPack повністю перетасованим. Перше завантаження гуляло між 176 і 182 КБ у збірках одного й того самого коміту, і перевірка бюджету на віддані байти падала випадково. Тому BIGBOARD рахує бюджет за розмірами до обфускації, а на те, що віддається, ставить стелю в 256 КБ із великим запасом. Саму причину усунула б мітка часу з коміту ([`git log -1 --format=%cI`](https://git-scm.com/docs/git-log)).

## Навіщо випускати іншу програму з кожним релізом?

Щоб чит, патч чи хук, написаний під одну збірку, переставав підходити до наступної. З [`seed: "git"`](https://www.afterpack.dev/docs/config#seed) кожен коміт дає структурно інший результат, а той самий seed із тими самими вхідними даними [дає ті самі байти](https://www.afterpack.dev/docs/builds#random-seed-by-default-pin-for-reproducibility). Із зафіксованою міткою часу дві збірки BIGBOARD одного коміту збіглися в усіх 140 файлах. Тоді пайплайн, що перезбирає проєкт для продакшну, може випустити рівно ті байти, які протестували на staging.

[Модель загроз](https://www.afterpack.dev/docs/threat-model#what-afterpack-does-not-replace) AfterPack описує цей ефект на пресеті `hard`: автор [читу](https://en.wikipedia.org/wiki/Cheating_in_online_games) «у кожному релізі наново проводить реальний аналіз, замість того щоб один раз пропатчити відоме зміщення». На `light` імена, зміщення й декодери рядків так само змінюються з кожним seed, просто кожен раунд аналізу дешевший. Так чи інакше, гра, що часто випускає релізи, змушує автора читу платити знову з кожним релізом. Скрипт, якому нова форма потрібна частіше, ніж ви деплоїте, може отримувати її [на кожен запит із Cloudflare Worker](https://www.afterpack.dev/blog/obfuscate-javascript-cloudflare-workers).

Із результатом, що змінюється з кожною збіркою, є одна пастка, про яку варто знати. AfterPack працює після того, як Vite уже назвав чанки, тож чанк може змінити свої байти, не змінивши імені файлу. Кеш [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), спільний для кількох збірок, тоді може віддати застарілий чанк поруч зі свіжим. Service worker у BIGBOARD називає кеш за збіркою і видаляє старий кеш лише тоді, коли жодна відкрита сторінка вже не працює на тій збірці.

## Як перевірити, що кожен випущений файл обфусковано?

Запускайте [`npx afterpack verify dist`](https://www.afterpack.dev/docs/cli#afterpack-verify-dir) останнім кроком перед деплоєм. Команда заново хешує кожен файл, названий у квитанції захисту збірки, і завершується помилкою, якщо файл змінився, якщо квитанції немає або якщо вона з іншої збірки.

BIGBOARD запускає її на кожному pull request і ще раз у скрипті деплою, одразу після збірки, як радить [посібник із CI](https://www.afterpack.dev/docs/builds#the-command-surface). Квитанція також фіксує, що навмисно лишили без обфускації:

```json
{
  "tool": "afterpack-vite",
  "engine": "local",
  "engineVersion": "0.2.3",
  "seedOrigin": "git",
  "files": [
    { "path": "assets/field-hockey-CK8_TNd4.js", "sha256": "0e5bf72c…", "transformed": true },
    { "path": "assets/rapier-B0bbuRDd.js", "sha256": "0f45e3d1…", "transformed": false }
  ]
}
```

## Від чого обфускація не захищає в мультиплеєрній грі?

Від рішучого читера вона не захистить. Вона сповільнює читання й патчинг клієнта і змушує починати цю роботу наново з кожним релізом, але не вирішує, хто забив. У peer-to-peer грі немає сервера, який би перевіряв, тож клієнт, модифікований кимось, у кого вистачає часу, однаково може збрехати про те, де шайба.

Для BIGBOARD.GAMES ставки низькі, а античит соціальний: суперник сидить навпроти за тим самим столом і дивиться на ту саму шайбу. Грі, де на кону рейтинги чи гроші, потрібен [авторитетний сервер](https://www.gabrielgambetta.com/client-server-game-architecture.html), який стоїть за клієнтом і перевіряє влучання, ходи й результати, про що каже й [модель загроз](https://www.afterpack.dev/docs/threat-model#not-a-substitute-for-server-authoritative-validation) AfterPack. Обфускація підвищує вартість читання коду, який ці перевірки не можуть покрити, а секрет, відправлений у браузер, не захистить ніщо: API-ключу в бандлі місце на сервері.

## Спробуйте на своїй грі

Додайте плагін у свою збірку або запустіть `npx afterpack@latest dist/` на результаті будь-якого бандлера ([швидкий старт](https://www.afterpack.dev/docs/quickstart)), а тоді відкрийте результат поруч з оригіналом у [DevTools](https://developer.chrome.com/docs/devtools). Безкоштовний [сканер безпеки](https://www.afterpack.dev/security-scanner) покаже, що ваша опублікована гра видає вже сьогодні. [BIGBOARD.GAMES](https://bigboard.games/uk/) безкоштовна, і найкраще в неї грати з двома планшетами й другом.

## Часті запитання

### Чи можна обфускувати гру на Phaser, PixiJS або Three.js?

Так. AfterPack працює із зібраним JavaScript, хоч яким рушієм чи фреймворком його створено, зокрема [Phaser](https://phaser.io/), [PixiJS](https://pixijs.com/) і [Three.js](https://threejs.org/). Поводьтеся з бібліотекою рушія так, як BIGBOARD поводиться з Rapier: винесіть її в окремий чанк, [виключіть її](https://www.afterpack.dev/docs/config#paths-exclude) й обфускуйте код своєї гри.

### Чи обфускує AfterPack WebAssembly?

Поки що ні. Зараз AfterPack перетворює JavaScript, а [WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly) — це напрям, у якому ми плануємо його розвивати. Фізика BIGBOARD працює у WebAssembly від Rapier, а це сторонній код, і він іде в продакшн як є; ігрова логіка довкола нього написана на JavaScript, і обфускується саме вона.

### Чи працює обфускація з PWA та service worker?

Так. BIGBOARD.GAMES встановлюється як [прогресивний вебзастосунок](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) і відкривається офлайн. Називайте кеш service worker за збіркою, бо обфусковані чанки можуть змінювати байти, не змінюючи імен файлів.

### Чи безкоштовний AfterPack для розробників ігор?

[Рушій Free](https://www.afterpack.dev/docs/tiers) працює локально, без акаунта й без обмежень, на будь-якому пресеті, і BIGBOARD.GAMES збирається саме ним. [Pro](https://www.afterpack.dev/docs/pro#what-pro-buys) додає [директиви](https://www.afterpack.dev/docs/directives) для окремих ділянок коду, як-от важчий пресет лише для мережевого коду, і дві [трансформації посилення](https://www.afterpack.dev/docs/config#transforms-kind-enabled): від $49 на місяць або разовими поповненнями ([ціни](https://www.afterpack.dev/docs/tiers#pricing)).

### Який пресет обрати для гри?

Почніть із [`light`](https://www.afterpack.dev/docs/presets#the-default-is-light) для всього бандла: це пресет за замовчуванням, і саме з ним виходить BIGBOARD.GAMES. [`medium`](https://www.afterpack.dev/docs/presets#the-ladder) усередині циклу кадрів я ще не міряв. Наступна гра, [«Нахили» (Tilt Run)](https://bigboard.games/uk/games/tilt-run), на кожному кадрі зчитує гіроскоп на всіх пристроях за столом, а їх може бути до чотирьох, тож перевіркою стане саме вона.
