В Gravity Defied я ни разу не играл, но решил собрать её заново — в браузере и без единой библиотеки. Получилась история о том, как из пружин и пары формул собрать мотоцикл, на котором хочется сделать ещё одну попытку.Всё началось с того, что мы с другом сели за PS5, и он запустил Trials Rising. После сотни попыток на одной трассе мне стало интересно, откуда у этого жанра растут ноги. Сам я рос на Hill Climb Racing на iPod, так что идея была знакомой, а вот первоисточник — нет. Ответ нашёлся быстро. Gravity Defied когда-то помещалась в память кнопочного телефона и держалась на одной простой мысли: мотоцикл нужно чувствовать, а любая неточность в наклоне заканчивается ударом головой о землю. Читать далее
Уровень сложностиСредний
Время на прочтение11 мин
Охват и читатели9.9K
Кейс

В этой статье я соберу свой аналог Gravity Defied для браузера — той самой мобильной игры про мотоциклиста‑стикмена. Игра написана на чистом JavaScript и Canvas, без сторонних библиотек, и работает как на компьютере, так и на телефоне, где управление сделано через джойстик — по итогам прошлой статьи.
Статья состоит из нескольких частей. Во введении коротко расскажу о мотивации и о самой игре. В основной части разберу создание клона: сначала то, что видит игрок, потом внутреннюю кухню и сложности, с которыми я столкнулся. В конце подведу итог и честно скажу, в чём моя версия пока не дотягивает до оригинала. Ссылки на игру и репозиторий — в конце.
ВведениеИгра, в которую я не игралОбычно я начинаю статью с истории о том, как играл в эту игру в прошлом, но здесь у меня возникла дилемма: признаюсь сразу, в Gravity Defied в детстве я не играл — от слова совсем. Из похожего у меня был iPod touch четвёртого поколения, и на нём я играл в Hill Climb Racing: машинка, холмы и бесконечные попытки заехать чуть дальше, чем в прошлый раз. В моём понимании это очень похоже на логику Gravity Defied.
Если вы спросите, зачем тогда браться за игру, в которую не играл, то ответ такой: недавно мы с другом сели поиграть на PS5, и у него была Trials Rising — тот же мотоцикл, физика и сотня попыток на одной трассе. После нашей встречи мне стало интересно, почему я раньше ничего не слышал об этой игре и откуда у такого жанра растут ноги. Буквально первая же ссылка подсказала, что культовая игра такого типа — Gravity Defied.
Ну и мысль пришла сама собой: если я уже писал Flappy Bird, Pac‑Man и тамагочи, то почему бы не попробовать и мотоцикл? Делать ремейк игры, которую ни разу не запускал, — затея странная, но с тамагочи было так же, и это ничуть не помешало: никакой ностальгии — только вопрос «а как это вообще работает».
Немного истории
Gravity Defied
Gravity Defied сделала студия Codebrew Software, а начиналась игра как проект для конкурса Excitera Mobile Awards 2004. На финале в Стокгольме в мае 2 004 года она, тогда ещё под рабочим названием A‑Trial, получила главную награду — Best in Show. Демоверсию, вышедшую в конце августа того же года, скачали больше 5 тыс. раз, а полная версия вышла 2 апреля 2 005 года (Википедия, дата обращения: 14.10.2026).
Для запуска игры нужна была только поддержка J2ME и около 80 КБ памяти, поэтому она шла почти на любом кнопочном телефоне того времени. Дальше игра зажила своей жизнью: с 2 007 года фанаты выпускали для неё редакторы трасс и модификации, в 2 008 году Codebrew анонсировала вторую часть, которая так и не вышла, а в 2 012 году появилась официальная версия для Android (Википедия, дата обращения: 14.10.2026).
Сами разработчики вспоминают финал конкурса так:
«The finals were tough with many good entries but the game A‑Trial finally came out on top and won the best‑in‑show award».
«Финал был тяжёлым, сильных работ было много, но A‑Trial в итоге вышла на первое место и получила главную награду конкурса (best‑in‑show)».
— Codebrew Software, раздел «About the project» на официальном сайте игры (архивная копия, дата обращения: 14.10.2026)
Выходит, игра выросла из небольшого конкурсного проекта, а держится на одной идее — мотоцикле, которым нужно аккуратно управлять. Отсюда и цель статьи: разобраться, как такая игра может быть устроена, и собрать её заново, от картинки на экране до физики под капотом.
Основная частьЧто видит игрокВот так выглядит заезд по первой трассе:

Заезд по первой трассе
Задача та же, что в оригинале: доехать до финиша как можно быстрее и не удариться головой. Трассы разбиты на три лиги — «Новичок», «Любитель» и «Профи», причём следующая трасса открывается только после предыдущей, а пройденная лига открывает новый мотоцикл. Лучшее время по каждой трассе хранится в браузере.

меню
Во время заезда на экране остаются только время и две кнопки, потому что подсказки по управлению находятся в отдельном экране «Правила». На компьютере W — газ, S — тормоз, A и D наклоняют мотоцикл назад и вперёд, R перезапускает трассу, а Esc ставит паузу.
На телефоне вместо клавиш появляется джойстик. О нём расскажу чуть позже (ниже).

Мобильная версия

Авария
Как устроен проектСтруктура такая:
index.html
css/style.css
js/
├── main.js, config.js — точка входа и все настройки
├── physics.js — векторы, точки, колёса, пружины, столкновения
├── input.js — клавиатура и джойстик
├── entities/ — мотоцикл, гонщик, авария
├── track/ — сборка трассы и уровни
├── game/ — правила игры, игровой цикл, рекорды и призрак
├── render/ — камера и вся отрисовка
├── ui/ — таймер и экраны
└── editor/ — редактор трасс
Физика ничего не знает про отрисовку, а отрисовка — про клавиатуру. Игра только связывает всё вместе, а все числа, от которых зависит поведение мотоцикла, лежат в config.js, так что подкручивать физику можно, не трогая остальной код.
Мотоцикл из точек и пружинМотоцикл здесь — не картинка и не жёсткий прямоугольник, а четыре точки с массой: два колеса, рама и голова гонщика, связанные шестью пружинами. Там, где у настоящего мотоцикла металл, пружины жёсткие, а там, где подвеска, — мягкие. Голова тоже физическая точка. Поэтому отдельно проверять, не перевернулся ли мотоцикл, не нужно: коснулся головой земли — авария.
Три мотоцикла, один классМотоциклы отличаются только параметрами: есть базовая модель, а остальные меняют в ней то, что нужно.
const BASE_MODEL = {
power: 3000,
topSpeed: 780,
leanTorque: 6,
flipSpeed: 9,
size: 1,
color: '#d32f2f',
trim: '#8e1c1c',
extras: [],
};
export const BIKE_MODELS = [
{ name: '100cc' },
{
name: '175cc',
power: 3600,
topSpeed: 930,
flipSpeed: 8.5,
size: 1.06,
color: '#1e88e5',
trim: '#0d47a1',
extras: ['fender'],
},
// 220cc устроен так же
].map(model => ({ ...BASE_MODEL, ...model }));
Чем мощнее мотоцикл, тем резче разгон и выше скорость, и жёлтый 220 cc уходит от красного уже на первых секундах. Кроме цвета, мотоциклы различаются размером и деталями: у синего есть переднее крыло, у жёлтого — обтекатель. Размер при этом меняет не только картинку, но вместе с ней растут колёса и рама в физике.

100 cc, 175 cc и 220 cc
Как колесо цепляется за землюМотор ничего не толкает напрямую — он только раскручивает заднее колесо, а дальше всё решает трение:
applyContact() {
if (!this.contact) {
return;
}
const tangent = this.contact.perpendicular();
const rimSpeed = this.spin * this.radius;
const slip = this.velocity.dot(tangent) - rimSpeed;
const impulse = slip * CONFIG.WHEEL_GRIP / 2;
this.accelerate(tangent.scale(-impulse));
this.spin += impulse / this.radius;
}Если обод крутится быстрее, чем колесо едет по земле, разница делится пополам: одна половина разгоняет колесо, а другая замедляет его вращение. Из‑за этого мотоцикл буксует на старте. Тормоз просто гасит вращение колёс и останавливает мотоцикл через то же трение, а при резком торможении может задрать заднее колесо.
Шаг физикиВесь мир обновляется одним методом:
step(body, track, dt) {
const { points, springs } = body;
points.forEach(point => point.integrate(this.gravity, dt));
springs.forEach(spring => spring.prepare());
for (let i = 0; i < CONFIG.SOLVER_ITERATIONS; i++) {
springs.forEach(spring => spring.solve(dt));
points.forEach(point => {
resolveCollision(point, track, body.loopSide);
});
}
points.forEach(point => point.updateVelocity(dt));
springs.forEach(spring => spring.damp());
points.forEach(point => point.applyContact());
body.loopSide = track.loopSide(body.center(), body.loopSide);
Это упрощённая схема position based dynamics. Точки сначала двигаются по инерции, затем за несколько проходов пружины и столкновения поправляют их положение, и только после этого по новым позициям считаются скорости, к которым применяются амортизаторы и трение. Последняя строчка нужна для мёртвой петли, о ней ниже. Шаг при этом фиксированный — 120 раз в секунду независимо от частоты обновления экрана, потому что иначе на мониторе 144 Гц и на телефоне с 60 Гц мотоцикл вёл бы себя по‑разному.
Наклон и сальтоГлавной причиной популярности оригинала автор обзора на Ferra.ru называет именно управление, где всё решает точность наклона:
«Это был не минус, а величайший из плюсов, потому что малейшая неточность, крошечная ошибка в наклоне могли привести к тому, что тело главного героя коснётся земли.»
— Денис Зайченко, «Gravity Defied Original — Чистопородный мобильный винтаж», Ferra.ru, 14 июня 2 014 года (дата обращения: 14.10.2026)
Поэтому на наклон у меня ушло больше всего времени. В коде он выглядит так:
lean(target, dt) {
const limit = CONFIG.LEAN_SPEED * dt;
const change = target - this.leanAmount;
this.leanAmount += clamp(change, -limit, limit);
const shift = this.leanAmount * CONFIG.LEAN_SHIFT;
this.rearToHead.stretch(1 + shift);
this.frontToHead.stretch(1 - shift);
this.rotate(this.isFlying()
? this.flipTurn(dt)
: this.leanAmount * this.model.leanTorque * dt);
}
flipTurn(dt) {
const wanted = this.leanAmount * this.model.flipSpeed;
const limit = CONFIG.FLIP_TORQUE * dt;
return clamp(wanted - this.rotationSpeed(), -limit, limit);
}
На земле наклон работает за счёт двух эффектов сразу. Гонщик переносит вес, потому что одна пружина от колеса до головы немного удлиняется, а другая укорачивается, и одновременно весь мотоцикл получает небольшой поворот вокруг центра масс, благодаря которому можно встать на заднее колесо.
Сложнее всего было подобрать, насколько сильно гонщик смещает вес. Голова, рама и колёса образуют высокий, узкий треугольник, в котором даже небольшое изменение длины пружин заметно сдвигает голову назад. При слишком сильном смещении мотоцикл с газом и наклоном назад переворачивался прямо с места, при слишком слабом наклоне почти ничего не менялось.
С сальто была отдельная история: в первой версии их почти невозможно было сделать, потому что в воздухе мотоцикл вращался медленно, а трамплины подбрасывали его меньше чем на полсекунды. Теперь в воздухе наклон задаёт скорость вращения, пока держишь клавишу, мотоцикл крутится, а стоит её отпустить — вращение быстро гаснет.
Скорость у каждого мотоцикла своя: синий крутится медленнее всех, жёлтый быстрее.
Трасса из кубиковРисовать трассы по точкам вручную мне не хотелось, поэтому каждый уровень — это просто список участков:
{
id: 'warmup',
name: 'Разминка',
sections: [
{ type: 'hill', length: 600, height: 40 },
{ type: 'flat', length: 200 },
{ type: 'waves', count: 4, length: 150, height: 10 },
{ type: 'flat', length: 200 },
{ type: 'smoothSlope', length: 500, height: 80 },
// ...
],
}
Конструктор трасс превращает каждый участок в набор точек: холм строится по косинусу, а трамплин — по параболе, которая заканчивается обрывом:
hill({ length, height }) {
this.curve(length, ratio => height * fullWave(ratio));
}
jump({ length, height }) {
this.curve(length, ratio => height * ratio * ratio);
this.lineTo(0, -height);
}

Холм
СтолкновенияТрасса — это ломаная из сотен коротких отрезков, и проверять каждое колесо со всеми отрезками на каждом шаге было бы расточительно, поэтому трасса заранее разложена на вертикальные полосы шириной 100 пикселей, а точка проверяет только свою полосу. Само столкновение сводится к тому, чтобы вытолкнуть точку из ближайшего отрезка на расстояние её радиуса:
const isInner = ratio > 0 && ratio < 1;
const isBehind = isInner && offset.dot(segment.normal) < 0;
const normal = isBehind ? segment.normal : offset.scale(1 / distance);
point.moveTo(closest.add(normal.scale(point.radius)));
point.touch(normal, segment.isDeadly);
Строчка с isInner выглядит безобидно, но появилась она после одного неприятного бага, о котором расскажу ниже.
Когда ставишь рекорд, заезд записывается: двадцать раз в секунду сохраняется положение колёс, рамы и головы. В следующий раз рядом едет полупрозрачная копия, и сразу видно, на каком участке теряешь время. Между записанными кадрами положение достраивается плавно, поэтому призрак не дёргается.

Призрак уехал вперёд
Управление: клавиши и джойстикКлавиатура слушает не буквы, а физические клавиши:
const CONTROL_KEYS = {
gas: ['KeyW', 'ArrowUp'],
brake: ['KeyS', 'ArrowDown'],
leanBack: ['KeyA', 'ArrowLeft'],
leanForward: ['KeyD', 'ArrowRight'],
};
Если использовать event.key, то на русской раскладке вместо w приходит ц и игра просто не реагирует, а event.code от раскладки не зависит.
На телефоне, по итогам прошлой статьи, вместо кнопок теперь джойстик. Он возвращает направление от −1 до 1 по каждой оси, а в центре есть мёртвая зона, чтобы палец, просто лежащий на джойстике, не включал газ:
function axisValue(value) {
const deadZone = CONFIG.JOYSTICK_DEAD_ZONE;
if (value <= deadZone) {
return 0;
}
return Math.min((value - deadZone) / (1 - deadZone), 1);
Клавиатура и джойстик работают одновременно: от каждого источника берётся максимальное значение.
Что оказалось сложнымСложнее всего было не написать физику, а заставить её вести себя правдоподобно. Первая версия пружин при нескольких проходах становилась почти жёсткой, и подвеска под весом гонщика не проседала вообще, поэтому пружины пришлось переписать на XPBD — вариант, где жёсткость задаётся в физических единицах и не зависит от числа проходов. Потом я долго искал в физике ошибку, из‑за которой мотоцикл разбивался в первой же низине второго уровня. Ошибка оказалась в трассе: края низины изгибались так резко, что на скорости мотоцикл честно отрывался от земли и переворачивался. Был и баг на краю трамплина, где колесо, оказавшись за вертикальным отрезком обрыва, выталкивалось вперёд сквозь воздух, и мотоцикл внезапно разгонялся в разы.
С новыми возможностями пришли новые проблемы, так как сальто вначале не было (почти), то потом пришлось менять вращение в воздухе и переделывать трамплины, чтобы в игре можно было спокойно делать сальто. Потом появилась дрожь картинки, и она стала заметна на быстрых мотоциклах. Ушла, только когда положение начали рисовать между шагами физики. Ящики превращались в тупик, пока у них не появились скошенные боковины, а петля пересекала сама себя, пока её не разделили на две половины.
Заключение
финал
Цель была простая: разобраться, как устроена игра, которой хватало 80 КБ памяти кнопочного телефона, и собрать её заново на чистом JavaScript — для компьютера и для телефона. Получилась игра с тринадцатью трассами, тремя мотоциклами и редактором. Больше всего мне запомнился момент, когда стало понятно, что редактор работает корректно и можно создавать свою карту.
Все тринадцать трасс я прошёл сам. Вот меню с временем по каждой (чтобы не писали, что пройти нельзя):

меню с временем по каждой:
От оригинала моя версия всё равно отличается: физику я писал с нуля, не разбирая движок Codebrew, так что управление наверняка ощущается иначе.
Если хочется именно оригинал, с родной физикой и трассами, есть порт [gravity_defied_cpp] на C++, собранный по коду оригинальной игры, а на его основе сделана браузерная версия (не реклама, просто, когда искал информацию, наткнулся на этот репозиторий и, надеюсь, автор не против).
В этой игре мне больше всего нравится, что борешься не столько с трассой, сколько с собственным мотоциклом. Никакого рандома, только твои руки и гравитация, и, похоже, именно на этом держатся и Hill Climb Racing, и Trials Rising.
Код лежит на GitHub, поиграть можно прямо в браузере.
А что вы думаете о Gravity Defied?
P. S. Если найдёте баг, придумаете, какой трассы не хватает, или просто захотите похвастаться рекордом — пишите в комментариях, как и в прошлый раз.
UPD. Как подсказали в комментариях, первоисточник жанра — Action SuperCross (1997) и Elasto Mania (2000) от одного разработчика. До статьи я про них не слышал, так что, если вам тоже интересно, — почитайте про обе, я в ближайшее время займусь этим сам.
© 2026 ООО «МТ ФИНАНС»
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | НЕ ЗАБАЛУЕШЬ MotoGP наконец-то признали то, что стартовые "приседалки" (front ... | -3 | 7 | 07-07-2026 |
| 2 | Хидео Кодзима, создатель Metal Gear Solid и Death Stranding, рассказал, ... | 0 | 6.02 | 21-10-2026 |
| 3 | 🌞 Рубрика: #ГравитационныйКвест🛸 🌏 Космонавты на МКС парят в невесомости. ... | 0 | 5 | 01-07-2026 |
| 4 | o2 game engine | 1 | 6.4 | 24-08-2026 |
| 5 | CFMoto's liter-class adventure bike lands in the USA for $12,499 | 0 | 14.36 | 22-08-2026 |
| 6 | Fresh Tracks VR Impression: A Wintry Mix Of Ski And Sorcery | 0 | 6.4 | 06-10-2026 |
| 7 | Frozen Fever: The Deus Swank Rally on Ice XSR900 by La Manufacture F | 0 | 12.07 | 20-04-2026 |
| 8 | [Перевод] Чему десять лет в разработке научили меня о технологиях | 0 | 12.49 | 06-08-2026 |
| 9 | Игра просто космос👨🚀 | 0 | 4.44 | 31-07-2026 |