Гравитация и прыжок

Учим цыплёнка падать вниз под собственным весом и красиво подпрыгивать по нажатию клавиши — за этим стоят всего две переменные и одно сложение каждый кадр.

Гравитация — это постоянное ускорение вниз, которое каждый кадр добавляется к вертикальной скорости героя. Прыжок — это просто резкий толчок вверх: на один миг мы задаём скорости большое отрицательное значение, а дальше гравитация сама плавно гасит взлёт и тянет цыплёнка обратно к земле.

Добро пожаловать в платформеры — жанр Марио, Соника, Hollow Knight и доброй половины игр, в которые ты залипал. Всё, что делает платформер платформером, начинается с одной фразы: герой падает. Он не висит в воздухе, как мяч в Понге, а тянется к земле, и потому может прыгать, перепрыгивать ямы и приземляться на платформы. Сегодня мы научим нашего цыплёнка чувствовать собственный вес.

В прошлых уроках цыплёнок уже умеет двигаться: ты разбирал дельта-время, чтобы движение было ровным при любом FPS, и управление с клавиатуры, чтобы герой слушался стрелок. Теперь мы добавим ему вертикальную физику — и цыплёнок впервые подпрыгнет.

Зачем это нужно

Представь Марио без гравитации. Ты жмёшь «прыжок» — и он улетает вверх и не возвращается. Или вообще не прыгает, а парит в воздухе на одной высоте. Скучно, правда? Вся магия прыжка в том, что он заканчивается: герой взлетает, замедляется в верхней точке, на миг зависает и плавно падает обратно. Эта красивая дуга — не случайность и не хитрая анимация. Это физика, и сегодня ты запрограммируешь её сам.

К концу урока у тебя на холсте будет жёлтый цыплёнок, стоящий на земле. Жмёшь пробел — он резко подскакивает вверх, в верхней точке как будто на мгновение замирает, а потом всё быстрее падает обратно и мягко приземляется на пол. Жмёшь ещё раз — снова прыжок. Это уже не квадратик, который телепортируется по нажатию: это герой, который чувствует вес и подчиняется гравитации, как настоящий.

И самое приятное: всё это держится буквально на двух новых переменных и одной строчке сложения. Никакой страшной физики из учебника — только идея «скорость каждый кадр чуть-чуть растёт вниз». Освоишь её здесь — и будешь пользоваться в каждой своей платформенной игре.

Скорость и ускорение: в чём разница

Прежде чем писать код, нужно развести два понятия, которые новички постоянно путают: скорость и ускорение. От этой путаницы рождается половина багов в платформерах, поэтому давай разберёмся раз и навсегда.

Скорость — это насколько объект сдвинется за один кадр. Ускорение — это насколько изменится сама скорость за один кадр. Скорость двигает координату, ускорение двигает скорость.

Метафора: представь, что ты в машине. Скорость — это цифра на спидометре, например 60 км/ч: именно с ней ты сейчас едешь. Ускорение — это насколько ты давишь на педаль газа: пока педаль нажата, цифра на спидометре растёт. Отпустил газ — ускорения нет, скорость держится. Газ в пол — скорость всё растёт и растёт.

Гравитация — это и есть постоянно нажатая педаль газа, направленная вниз. Она не двигает цыплёнка сама по себе. Она каждый кадр чуть-чуть увеличивает его скорость падения, а уже эта скорость двигает координату y. Вот почему падающие тела ускоряются: чем дольше падаешь, тем быстрее летишь. Урони телефон с дивана и с пятого этажа — во втором случае он успеет разогнаться куда сильнее (только не проверяй).

В коде нам понадобятся две вещи, которые ты уже знаешь по глоссарию: вектор скорости (точнее, его вертикальная часть vy — насколько цыплёнок сдвигается по вертикали за кадр) и гравитация (число, которое мы каждый кадр прибавляем к vy). Всё взаимодействие — три строчки:

chicken.vy += gravity;   // ускорение меняет скорость
chicken.y  += chicken.vy; // скорость меняет координату
// и так каждый кадр

Результат: кода-картинки тут пока нет — это сердцевина физики, которую мы сейчас обернём в полноценный пример. Первая строка — «педаль газа»: vy с каждым кадром становится чуть больше. Вторая строка — само движение: цыплёнок сдвигается вниз ровно на текущую скорость. Поскольку vy постоянно растёт, каждый следующий сдвиг чуть длиннее предыдущего — падение разгоняется.

Шаг за шагом: учим цыплёнка падать

Не будем сразу делать прыжок — сначала пусть цыплёнок просто упадёт. Это самый честный способ убедиться, что физика работает.

Пример 1. Свободное падение

Берём нашего сквозного героя chicken и добавляем ему поле vy — вертикальную скорость. В начале она равна нулю: цыплёнок неподвижен. Каждый кадр прибавляем к ней гравитацию и сдвигаем цыплёнка на эту скорость вниз.

const canvas = document.getElementById('game');
const context = canvas.getContext('2d');

// сквозной герой курса — наш цыплёнок
// vy — вертикальная скорость, в начале он висит неподвижно
const chicken = { x: 180, y: 40, width: 40, height: 40, vy: 0 };

const gravity = 0.5; // насколько растёт скорость падения каждый кадр

function update() {
  chicken.vy += gravity;     // гравитация ускоряет падение
  chicken.y  += chicken.vy;  // скорость двигает цыплёнка вниз
}

function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = '#ffdc3c';
  context.fillRect(chicken.x, chicken.y, chicken.width, chicken.height);
}

function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

loop();

Результат: жёлтый цыплёнок появляется у верхнего края холста и начинает падать вниз — сначала медленно, потом всё быстрее, разгоняясь с каждым кадром. Очень скоро он улетает за нижний край и исчезает: пола ведь ещё нет, останавливаться не обо что. Но главное мы увидели — падение не равномерное, а ускоряющееся, ровно как у настоящего предмета под действием тяжести.

Разберём по строчкам:

  • vy: 0 — в начале цыплёнок неподвижен по вертикали. Скорость нулевая, но гравитация тут же начнёт её раскручивать.
  • chicken.vy += gravity — каждый кадр скорость падения увеличивается на 0.5. После первого кадра vy = 0.5, после второго 1.0, потом 1.5, 2.0… Скорость растёт линейно — это и есть ускорение.
  • chicken.y += chicken.vy — координата сдвигается на текущую скорость. Раз скорость растёт, то и шаг падения растёт: цыплёнок проваливается всё стремительнее.

Обрати внимание: gravity мы прибавляем к скорости, а не к координате напрямую. Если бы мы писали chicken.y += gravity, цыплёнок падал бы с одной и той же скоростью, как лифт, — без разгона, неживо. Вся правдоподобность падения именно в том, что между гравитацией и координатой стоит промежуточное звено — скорость.

Пример 2. Добавляем пол

Падать в бездну скучно — цыплёнку нужна земля под ногами. Добавим пол: горизонтальную линию у нижнего края холста. Как только нижний край цыплёнка достигает пола, мы прижимаем его к полу и обнуляем скорость падения, иначе он будет проваливаться сквозь землю.

const chicken = { x: 180, y: 40, width: 40, height: 40, vy: 0 };
const gravity = 0.5;
const floorY = 360; // координата y, на которой находится пол

function update() {
  chicken.vy += gravity;
  chicken.y  += chicken.vy;

  // нижний край цыплёнка — это chicken.y + chicken.height
  if (chicken.y + chicken.height > floorY) {
    chicken.y = floorY - chicken.height; // ставим точно на пол
    chicken.vy = 0;                       // падать больше некуда
  }
}

function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);

  // пол
  context.fillStyle = '#3a7d44';
  context.fillRect(0, floorY, canvas.width, canvas.height - floorY);

  // цыплёнок
  context.fillStyle = '#ffdc3c';
  context.fillRect(chicken.x, chicken.y, chicken.width, chicken.height);
}

Результат: теперь цыплёнок падает сверху, разгоняется — и с лёгким «шлепком» останавливается на зелёной полосе пола у нижнего края холста. Дальше он не проваливается и не дёргается, а спокойно стоит на земле. Физика падения отработала, пол его поймал.

Что здесь важно понять:

  • Мы сравниваем нижний край цыплёнка (chicken.y + chicken.height) с уровнем пола floorY. Не сам chicken.y — иначе цыплёнок наполовину утонет в земле.
  • Строка chicken.y = floorY - chicken.height аккуратно ставит героя ровно на пол. За один кадр он мог провалиться на пару пикселей ниже — мы возвращаем его точно на поверхность, чтобы он не дрожал.
  • chicken.vy = 0 — критически важная строка. Если её забыть, гравитация продолжит копить скорость, и в следующем кадре цыплёнок снова рванёт сквозь пол. Обнулив скорость, мы говорим: «приземлился, падение окончено».

Пример 3. Прыжок

Вот ради чего всё затевалось. Прыжок устроен до смешного просто: в момент нажатия клавиши мы задаём vy большое отрицательное значение — резкий толчок вверх (помни: в canvas ось y растёт вниз, поэтому «вверх» — это отрицательная скорость). Дальше нам ничего не надо делать руками: гравитация, которую мы уже написали, сама плавно погасит взлёт, остановит цыплёнка в верхней точке и потянет вниз. Прыжок и падение — один и тот же код.

Чтобы цыплёнок не прыгал в воздухе бесконечно, добавим флаг onGround: прыгать можно, только когда стоишь на земле.

const chicken = { x: 180, y: 40, width: 40, height: 40, vy: 0, onGround: false };
const gravity = 0.5;
const jumpForce = 12; // сила толчка вверх
const floorY = 360;

const keys = {};
window.addEventListener('keydown', function (e) { keys[e.code] = true; });
window.addEventListener('keyup',   function (e) { keys[e.code] = false; });

function update() {
  // прыжок: только если стоим на земле
  if (keys['Space'] && chicken.onGround) {
    chicken.vy = -jumpForce; // толчок вверх (минус = вверх)
    chicken.onGround = false;
  }

  chicken.vy += gravity;
  chicken.y  += chicken.vy;

  // приземление
  if (chicken.y + chicken.height > floorY) {
    chicken.y = floorY - chicken.height;
    chicken.vy = 0;
    chicken.onGround = true; // снова можно прыгать
  }
}

function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = '#3a7d44';
  context.fillRect(0, floorY, canvas.width, canvas.height - floorY);
  context.fillStyle = '#ffdc3c';
  context.fillRect(chicken.x, chicken.y, chicken.width, chicken.height);
}

function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

loop();

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

Самое красивое здесь — что взлёт и падение описаны одним и тем же кодом. Смотри, как разворачивается прыжок:

  • В момент нажатия vy = -12 — большая скорость вверх. Цыплёнок резко уходит наверх.
  • Каждый кадр vy += 0.5 постепенно гасит этот взлёт: -12, -11.5, -11… Скорость вверх тает.
  • Когда vy доходит до нуля — это верхняя точка прыжка. Цыплёнок на миг неподвижен по вертикали (тот самый «зависон»).
  • Дальше vy становится положительной и растёт — начинается обычное падение, которое мы уже разобрали. Гравитация одна и та же и на взлёте, и на спуске.

Флаг onGround — это маленький, но важный кусочек состояния игрока. Он отвечает на вопрос «можно ли сейчас прыгать?». При приземлении мы ставим его в true, при прыжке — в false. Без него получился бы баг, который ты наверняка встречал в кривых играх: герой прыгает прямо из воздуха, бесконечно набирая высоту.

Пример 4. Ограничиваем скорость падения

Есть тонкая проблема, которая всплывает, если цыплёнок падает с большой высоты. Гравитация копит скорость без остановки, и с очень высокой платформы vy может дорасти до огромных значений — например, до 40 или 60 пикселей за кадр. Тогда за один кадр цыплёнок перепрыгнет весь пол насквозь, и проверка приземления не успеет сработать. Герой проваливается под землю — классический баг «протыкания».

Решение простое: введём предел скорости падения. Как бы долго цыплёнок ни летел, его vy не превысит заданный потолок.

const maxFallSpeed = 15; // быстрее этого цыплёнок не падает

function update() {
  if (keys['Space'] && chicken.onGround) {
    chicken.vy = -jumpForce;
    chicken.onGround = false;
  }

  chicken.vy += gravity;

  // не даём скорости падения разогнаться сверх предела
  if (chicken.vy > maxFallSpeed) {
    chicken.vy = maxFallSpeed;
  }

  chicken.y += chicken.vy;

  if (chicken.y + chicken.height > floorY) {
    chicken.y = floorY - chicken.height;
    chicken.vy = 0;
    chicken.onGround = true;
  }
}

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

Это называется ограничение (clamp) скорости. В реальной физике у падения тоже есть такой предел — его дарит сопротивление воздуха, поэтому парашютист не разгоняется бесконечно. В играх же главная причина прозаичнее: уберечь героя от протыкания сквозь тонкий пол. Одна строчка if (chicken.vy > maxFallSpeed) chicken.vy = maxFallSpeed — и проблема закрыта.

Частые ошибки и подводные камни

Гравитация выглядит безобидно, но именно на ней спотыкаются почти все, кто впервые делает платформер. Вот грабли, которые сэкономят тебе вечер отладки.

1. Прибавлять гравитацию к координате, а не к скорости

Самая концептуальная ошибка. Если написать chicken.y += gravity вместо chicken.vy += gravity, цыплёнок будет падать с постоянной скоростью, как лифт, без всякого разгона. Пропадёт вся живость падения. Запомни цепочку: гравитация меняет скорость, скорость меняет координату. Между тяжестью и положением всегда стоит промежуточное звено — vy.

2. Забыть обнулить vy при приземлении

Если не написать chicken.vy = 0 в момент касания пола, гравитация продолжит копить скорость, даже пока цыплёнок «стоит». Накопленная скорость будет всё толкать его вниз, и на следующем кадре он провалится сквозь пол. Симптом: герой дрожит у земли или тонет в ней. Лечение — обнулять vy при каждом приземлении.

3. Перепутать знак прыжка

В canvas ось y растёт вниз, поэтому толчок вверх — это отрицательная скорость: vy = -jumpForce. Если случайно написать vy = jumpForce (без минуса), цыплёнок по нажатию пробела не подпрыгнет, а резко нырнёт вниз сквозь пол. Помни школьную интуицию «вверх — это плюс» здесь придётся перевернуть.

4. Разрешить прыжок без проверки onGround

Если прыгать можно в любой момент, а не только с земли, цыплёнок будет бесконечно набирать высоту, пока ты дёргаешь пробел в воздухе, — получится «вертолётик». Флаг onGround (или похожая проверка) обязателен: прыжок разрешён, только когда герой действительно стоит на поверхности.

5. Слишком большая или слишком маленькая гравитация

Числа gravity и jumpForce определяют, как ощущается прыжок. Если гравитация огромная — цыплёнок прыгает как камень, дуга получается резкой и неуклюжей. Если крошечная — герой парит в воздухе вечность, будто на Луне. Тут нет единственно правильных чисел: подбирай на ощупь, меняй по чуть-чуть и смотри, что приятнее. Хорошая отправная точка — то, что в примерах: gravity ≈ 0.5, jumpForce ≈ 12. Это «искусство чувства», и крутить эти две цифры — отдельное удовольствие.

Мини-проект: двойной прыжок и платформа

Теперь твоя очередь. Возьми код из примера 4 (цыплёнок, пол, прыжок, ограничение скорости) и прокачай его:

  1. Сделай прыжок выше или ниже. Поиграй с числами gravity и jumpForce. Поставь gravity = 0.3 — почувствуй «лунную» физику. Поставь jumpForce = 18 — цыплёнок будет подпрыгивать гораздо выше. Найди настройку, которая нравится именно тебе.
  2. Добавь двойной прыжок. Заведи счётчик jumpsLeft. На земле ставь ему значение 2. Каждый прыжок уменьшает счётчик на 1, а прыгать можно, пока jumpsLeft > 0. Тогда цыплёнок сможет прыгнуть второй раз прямо в воздухе — как во многих современных платформерах.
  3. Положи платформу. Добавь второй прямоугольник-платформу повыше пола и сделай так, чтобы цыплёнок мог на неё приземлиться (используй ту же логику проверки нижнего края, что и для пола). Это уже маленький уровень!

Подсказки, чтобы получилось:

  • Для двойного прыжка убери проверку onGround в условии прыжка и замени её на jumpsLeft > 0. А jumpsLeft = 2 восстанавливай в момент приземления на пол.
  • Чтобы избежать «зажатого» прыжка (герой прыгает каждый кадр, пока держишь пробел), запоминай, что клавиша уже была нажата, и прыгай только на новое нажатие. Это та же идея «нажал — отпустил», что в уроке про управление.
  • Для приземления на платформу хорошо приземляться только когда цыплёнок падает (vy > 0) — иначе он прилипнет к платформе снизу, пробивая её головой на взлёте.

Если цыплёнок прыгает дважды и уверенно приземляется на платформу — поздравляю, у тебя в руках главная механика платформера. В следующих уроках мы достроим вокруг неё целый уровень.

Итоги

Сегодня ты дал цыплёнку вес и научил его прыгать — и сделал это поразительно малыми средствами. Вот что теперь у тебя есть:

  • Разница скорости и ускорения — скорость двигает координату, ускорение двигает скорость. Гравитация — это ускорение, постоянно нажатая «педаль газа» вниз.
  • Гравитация в коде — каждый кадр chicken.vy += gravity, а затем chicken.y += chicken.vy. Падение разгоняется само собой.
  • Пол и приземление — сравниваем нижний край цыплёнка с уровнем пола, ставим героя ровно на поверхность и обнуляем vy.
  • Прыжок — это всего лишь резкое vy = -jumpForce. Взлёт и падение описаны одним и тем же кодом: гравитация сама гасит взлёт и тянет обратно.
  • Ограничение скорости падения — потолок для vy, чтобы цыплёнок не разогнался и не протыкал тонкий пол.

Главный принцип, который ты унесёшь: вся физика платформера держится на одной цепочке «ускорение → скорость → координата», пересчитываемой каждый кадр. Прыжок, падение, отскок — это всё она.

В следующем уроке мы построим вокруг прыгающего цыплёнка настоящий уровень из платформ и научим его не проваливаться сквозь них и приземляться сверху — с помощью AABB-столкновений, которые ты уже освоил. Гравитация и прыжок у тебя есть; пора дать цыплёнку, по чему скакать.

Проверьте себя
1. В чём разница между скоростью и ускорением?
AЭто одно и то же, просто разные слова
BСкорость двигает координату, а ускорение меняет саму скорость
CУскорение двигает координату, а скорость меняет ускорение
DСкорость — это только для горизонтального движения, ускорение — для вертикального
2. Почему гравитацию прибавляют к скорости (vy), а не сразу к координате y?
AТак короче писать код
BИначе JavaScript выдаст ошибку
CЧтобы падение разгонялось: скорость растёт каждый кадр, и шаг падения становится больше
DЧтобы цыплёнок падал с постоянной скоростью
3. Как в canvas задаётся толчок прыжка вверх?
AПоложительным значением vy, потому что вверх — это плюс
BОтрицательным значением vy, потому что ось y растёт вниз
CОбнулением координаты y
DУвеличением гравитации
4. Что произойдёт, если забыть написать chicken.vy = 0 в момент приземления?
AНичего, код будет работать как надо
BЦыплёнок прыгнет в два раза выше
CГравитация продолжит копить скорость, и цыплёнок провалится сквозь пол
DЦыплёнок перестанет двигаться по горизонтали
5. Зачем нужен флаг onGround?
AЧтобы цыплёнок двигался быстрее
BЧтобы разрешать прыжок только когда герой стоит на земле, а не в воздухе
CЧтобы рисовать пол на холсте
DЧтобы ограничивать скорость падения
6. Зачем ограничивать максимальную скорость падения (maxFallSpeed)?
AЧтобы прыжок был выше
BЧтобы цыплёнок при падении с большой высоты не разогнался так, что за один кадр проткнёт пол насквозь
CЧтобы гравитация работала только вниз
DЧтобы экономить память браузера