Гравитация и прыжок
Учим цыплёнка падать вниз под собственным весом и красиво подпрыгивать по нажатию клавиши — за этим стоят всего две переменные и одно сложение каждый кадр.
Гравитация — это постоянное ускорение вниз, которое каждый кадр добавляется к вертикальной скорости героя. Прыжок — это просто резкий толчок вверх: на один миг мы задаём скорости большое отрицательное значение, а дальше гравитация сама плавно гасит взлёт и тянет цыплёнка обратно к земле.
Добро пожаловать в платформеры — жанр Марио, Соника, 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 (цыплёнок, пол, прыжок, ограничение скорости) и прокачай его:
- Сделай прыжок выше или ниже. Поиграй с числами
gravityиjumpForce. Поставьgravity = 0.3— почувствуй «лунную» физику. ПоставьjumpForce = 18— цыплёнок будет подпрыгивать гораздо выше. Найди настройку, которая нравится именно тебе. - Добавь двойной прыжок. Заведи счётчик
jumpsLeft. На земле ставь ему значение2. Каждый прыжок уменьшает счётчик на 1, а прыгать можно, покаjumpsLeft > 0. Тогда цыплёнок сможет прыгнуть второй раз прямо в воздухе — как во многих современных платформерах. - Положи платформу. Добавь второй прямоугольник-платформу повыше пола и сделай так, чтобы цыплёнок мог на неё приземлиться (используй ту же логику проверки нижнего края, что и для пола). Это уже маленький уровень!
Подсказки, чтобы получилось:
- Для двойного прыжка убери проверку
onGroundв условии прыжка и замени её наjumpsLeft > 0. АjumpsLeft = 2восстанавливай в момент приземления на пол. - Чтобы избежать «зажатого» прыжка (герой прыгает каждый кадр, пока держишь пробел), запоминай, что клавиша уже была нажата, и прыгай только на новое нажатие. Это та же идея «нажал — отпустил», что в уроке про управление.
- Для приземления на платформу хорошо приземляться только когда цыплёнок падает (
vy > 0) — иначе он прилипнет к платформе снизу, пробивая её головой на взлёте.
Если цыплёнок прыгает дважды и уверенно приземляется на платформу — поздравляю, у тебя в руках главная механика платформера. В следующих уроках мы достроим вокруг неё целый уровень.
Итоги
Сегодня ты дал цыплёнку вес и научил его прыгать — и сделал это поразительно малыми средствами. Вот что теперь у тебя есть:
- Разница скорости и ускорения — скорость двигает координату, ускорение двигает скорость. Гравитация — это ускорение, постоянно нажатая «педаль газа» вниз.
- Гравитация в коде — каждый кадр
chicken.vy += gravity, а затемchicken.y += chicken.vy. Падение разгоняется само собой. - Пол и приземление — сравниваем нижний край цыплёнка с уровнем пола, ставим героя ровно на поверхность и обнуляем
vy. - Прыжок — это всего лишь резкое
vy = -jumpForce. Взлёт и падение описаны одним и тем же кодом: гравитация сама гасит взлёт и тянет обратно. - Ограничение скорости падения — потолок для
vy, чтобы цыплёнок не разогнался и не протыкал тонкий пол.
Главный принцип, который ты унесёшь: вся физика платформера держится на одной цепочке «ускорение → скорость → координата», пересчитываемой каждый кадр. Прыжок, падение, отскок — это всё она.
В следующем уроке мы построим вокруг прыгающего цыплёнка настоящий уровень из платформ и научим его не проваливаться сквозь них и приземляться сверху — с помощью AABB-столкновений, которые ты уже освоил. Гравитация и прыжок у тебя есть; пора дать цыплёнку, по чему скакать.