Платформы и приземление

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

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

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

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

Открой в голове любой платформер, в который ты играл — Mario, Geometry Dash, Celeste, да хоть мобильный раннер. Что в них общего? Герой стоит на земле, отталкивается и прыгает на следующую площадку. Без платформ платформер — это просто падение в бездну. Платформа — это та самая твёрдая поверхность, ради которой жанр и назвали.

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

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

Как цыплёнок понимает, что приземлился

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

Платформа в игре работает точно так же. Это тайл — прямоугольный кусочек поверхности с координатами x, y и размерами w, h. Цыплёнок приземляется на него, когда выполняются три условия одновременно:

  • цыплёнок падает, а не взлетает — его вектор скорости по вертикали направлен вниз (chicken.vy > 0);
  • нижний край цыплёнка дотянулся до верхнего края платформы — но не провалился слишком глубоко;
  • цыплёнок попадает в платформу по горизонтали — его ноги над доской, а не левее или правее неё.

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

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

Коллизия — это знакомый тебе AABB

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

Договоримся про систему координат, она у нас сквозная для всего курса. У прямоугольника x, y — это левый верхний угол, ось y растёт вниз (чем больше y, тем ниже на экране). Значит, нижний край цыплёнка — это chicken.y + chicken.h, а верхний край платформы — это просто platform.y. Когда низ цыплёнка достигает верха платформы, эти два числа сравниваются.

Пример 1. Описываем цыплёнка и платформы

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

const chicken = {
  x: 80, y: 0,        // левый верхний угол спрайта
  w: 40, h: 40,       // размеры цыплёнка
  vx: 0, vy: 0,       // вектор скорости (поx и по y)
  onGround: false     // стоит ли он сейчас на земле
};

// каждая платформа — прямоугольник {x, y, w, h}
const platforms = [
  { x: 0,   y: 360, w: 480, h: 40 },  // длинный пол снизу
  { x: 120, y: 280, w: 120, h: 20 },  // ступенька повыше
  { x: 300, y: 200, w: 120, h: 20 }   // и ещё выше
];

Результат: на экране пока ничего не двигается — мы лишь подготовили данные. В памяти теперь живёт цыплёнок со скоростью 0 и три платформы: широкий пол у самого низа холста и две короткие полочки-ступеньки выше. Новое поле onGround — это флаг: true, когда под лапками твёрдо, и false, когда цыплёнок в полёте.

Разберём ключевые поля:

  • vy — вертикальная часть вектора скорости. Гравитация будет каждый кадр увеличивать его, разгоняя падение; при приземлении мы обнулим его.
  • onGround — главный герой этого урока. Именно по нему мы решим, можно ли прыгать. Каждый кадр мы будем сбрасывать его в false и заново поднимать, если нашли платформу под ногами.
  • platforms — массив. Цыплёнок один, а платформ много, поэтому проверять придётся в цикле — каждую доску по очереди.

Пример 2. Ловим приземление сверху

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

const GRAVITY = 0.5;

function update() {
  // 1. гравитация разгоняет падение
  chicken.vy += GRAVITY;
  chicken.y += chicken.vy;

  // 2. до проверки считаем, что земли под ним нет
  chicken.onGround = false;

  // 3. перебираем все платформы
  for (const p of platforms) {
    const chickenBottom = chicken.y + chicken.h;
    const overX = chicken.x + chicken.w > p.x && chicken.x < p.x + p.w;

    if (chicken.vy > 0 && overX &&
        chickenBottom > p.y && chickenBottom < p.y + p.h) {
      chicken.y = p.y - chicken.h;  // ставим ровно на край
      chicken.vy = 0;               // гасим падение
      chicken.onGround = true;      // под лапками твёрдо
    }
  }
}

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

Это сердце урока, разберём по шагам:

  1. Гравитация и движение. chicken.vy += GRAVITY каждый кадр делает падение чуть быстрее, а chicken.y += chicken.vy сдвигает цыплёнка вниз на текущую скорость. Это та же физика, что в прошлом уроке.
  2. Сброс флага. chicken.onGround = false — мы заранее считаем, что цыплёнок висит в воздухе. Если ни одна платформа его не поймает, флаг так и останется false. Это важно: иначе цыплёнок, сошедший с края доски, навсегда останется «на земле».
  3. Условие приземления. Четыре проверки через && должны сработать вместе: chicken.vy > 0 (падает), overX (ноги над доской по горизонтали), chickenBottom > p.y (низ дошёл до верха платформы) и chickenBottom < p.y + p.h (но не провалился сквозь всю доску).
  4. Постановка на край. chicken.y = p.y - chicken.h — мы вручную выставляем цыплёнка так, чтобы его низ chicken.y + chicken.h оказался ровно на p.y. Без этой строки цыплёнок застрянет, утонув в доске на пару пикселей.
  5. Гасим и фиксируем. chicken.vy = 0 останавливает падение, а chicken.onGround = true разрешает следующий прыжок.

Обрати внимание на overX: это горизонтальная половина AABB-проверки. chicken.x + chicken.w > p.x значит «правый край цыплёнка правее левого края доски», а chicken.x < p.x + p.w — «левый край цыплёнка левее правого края доски». Оба вместе означают, что цыплёнок висит над платформой, а не сбоку от неё.

Пример 3. Прыжок только с земли

Теперь подключим управление. Прыжок — это резкий толчок вверх: мы задаём вектору скорости отрицательное vy (вверх — это уменьшение y). Но прыгать разрешим только когда onGround === true.

const JUMP_POWER = -11;   // минус — толчок вверх

window.addEventListener('keydown', (e) => {
  if (e.code === 'Space' && chicken.onGround) {
    chicken.vy = JUMP_POWER;   // оттолкнулись вверх
    chicken.onGround = false;  // оторвались от земли
  }
});

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

Главная строка — условие e.code === 'Space' && chicken.onGround. Без второй половины (&& chicken.onGround) цыплёнок прыгал бы в любой момент, даже зависнув в воздухе, — и игрок бесконечно «всплывал» бы вверх, нажимая пробел. Флаг onGround — это пропуск, который выдаётся только на земле и тут же сгорает при отрыве. А поскольку в update() мы каждый кадр заново ставим onGround = true при касании платформы, пропуск автоматически выдаётся снова, как только цыплёнок приземлится.

Пример 4. Рисуем сцену

Соберём всё на canvas. Через контекст 2D нарисуем каждую платформу прямоугольником и поверх — цыплёнка. Это часть «нарисовать кадр» в игровом цикле.

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

  // платформы — коричневые доски
  context.fillStyle = '#8a5a2b';
  for (const p of platforms) {
    context.fillRect(p.x, p.y, p.w, p.h);
  }

  // цыплёнок поверх
  context.fillStyle = '#ffd34e';
  context.fillRect(chicken.x, chicken.y, chicken.w, chicken.h);
}

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

Результат: на холсте видны три коричневые доски и жёлтый квадрат-цыплёнок. Цыплёнок падает сверху, встаёт на одну из платформ и, если жать пробел, перепрыгивает с нижнего пола на полочки повыше. Каждый кадр update двигает физику, а draw заново рисует картинку — это и есть игровой цикл, который держит requestAnimationFrame.

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

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

Приземление выглядит просто, но именно на нём новички ловят самые обидные баги. Вот что ломается чаще всего.

1. Забыть проверку «цыплёнок падает» (vy > 0)

Если убрать из условия chicken.vy > 0, цыплёнок будет «приземляться» и когда подпрыгивает к платформе снизу. Он ударится головой о доску и прилипнет к её нижней стороне, как муха к потолку. Проверка на падение гарантирует: ловим только тех, кто движется вниз.

2. Не ставить цыплёнка на край платформы

Очень частая ошибка — обнулить vy, но забыть строку chicken.y = p.y - chicken.h. Тогда цыплёнок остановится там, где его застукала проверка — обычно утонув в доске на несколько пикселей, потому что за кадр он сдвинулся вниз сразу на всю величину vy. Выглядит так, будто он провалился по щиколотку. Всегда выставляй y руками ровно на край.

3. Не сбрасывать onGround каждый кадр

Если поднять onGround = true при касании, но забыть в начале update() сбросить его в false, флаг останется поднятым навсегда. Цыплёнок сойдёт с края платформы и продолжит «прыгать в воздухе», потому что игра всё ещё думает, что он на земле. Сбрасывай флаг перед циклом по платформам и поднимай заново только при реальном касании.

4. Проверять только верх, без горизонтали (overX)

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

5. Высокая скорость «прошивает» тонкую платформу

Если цыплёнок падает очень быстро (большое vy), за один кадр он может перепрыгнуть тонкую доску целиком: в одном кадре он ещё над ней, в следующем — уже под ней, и условие chickenBottom < p.y + p.h ни разу не сработало. Для учебной сцены хватает не делать платформы слишком тонкими и не разгонять падение до сотен пикселей за кадр. По-взрослому это лечится «проверкой на пути» (continuous collision), но это тема для будущих уроков.

Мини-проект: полоса препятствий

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

  1. Расставь лесенку из платформ. Сделай 4-5 платформ так, чтобы они шли вверх и вправо, как ступеньки. Нижняя — широкий пол, остальные — короткие полочки, до каждой следующей цыплёнок должен дотягиваться одним прыжком.
  2. Добавь движение по горизонтали. Подключи стрелки влево/вправо (управление ты уже умеешь): по нажатию меняй chicken.vx, а в update() прибавляй chicken.x += chicken.vx. Теперь цыплёнок сможет разбегаться и допрыгивать до боковых полочек.
  3. Не дай цыплёнку убежать за экран. После сдвига по x зажми координату в границы холста через Math.max и Math.min, чтобы герой не уехал за левый или правый край.
  4. Добавь «финиш». Помести верхнюю платформу повыше и считай уровень пройденным, когда цыплёнок встал на неё (onGround и его y совпал с её краем). Выведи через context.fillText надпись «Готово!».

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

  • Подбери JUMP_POWER и расстояние между полочками так, чтобы прыжок честно доставал до следующей ступеньки, но не перелетал её. Меняй число и смотри результат — это и есть геймдизайн на ощупь.
  • Если цыплёнок проскакивает сквозь полочки, сделай их толще (h побольше) или уменьши GRAVITY — так за кадр он будет сдвигаться на меньшее число пикселей.
  • Когда заработает, сохрани этот код: функцию update() с проверкой платформ ты переиспользуешь в следующих уроках про монетки и врагов, не переписывая заново.

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

Итоги

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

  • Платформы — это прямоугольники {x, y, w, h} в массиве, которые мы перебираем в цикле каждый кадр.
  • Приземление ловим только сверху: цыплёнок должен падать (vy > 0), быть над платформой по горизонтали (overX) и его низ должен только что пройти верх доски.
  • При касании делаем три вещи: ставим цыплёнка ровно на край (y = p.y - h), обнуляем падение (vy = 0) и поднимаем флаг onGround.
  • Прыжок разрешён только с земли: кнопка срабатывает лишь при onGround === true, и флаг сбрасывается каждый кадр заново — никаких прыжков из воздуха.

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

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

Проверьте себя
1. Почему в условии приземления важна проверка chicken.vy > 0?
AЧтобы цыплёнок двигался быстрее
BЧтобы ловить только падение, а не удар головой о платформу снизу
CЧтобы платформа стала видимой
DЧтобы разрешить двойной прыжок
2. Что делает строка chicken.y = p.y - chicken.h при приземлении?
AСдвигает платформу под цыплёнка
BСтавит цыплёнка ровно на верхний край платформы, чтобы он не утонул в ней
CУвеличивает скорость падения
DУдаляет платформу из массива
3. Зачем в начале update() мы пишем chicken.onGround = false?
AЧтобы запретить прыжки навсегда
BЧтобы заново пересчитать флаг каждый кадр: нет касания — значит, в воздухе
CЧтобы обнулить координаты цыплёнка
DЭто необязательная строка
4. За что отвечает условие overX (chicken.x + chicken.w > p.x && chicken.x < p.x + p.w)?
AПроверяет, что цыплёнок над платформой по горизонтали, а не сбоку от неё
BСчитает площадь платформы
CЗадаёт силу прыжка
DВключает гравитацию
5. Почему прыжок проверяет chicken.onGround, а не только нажатие пробела?
AЧтобы прыжок был выше
BЧтобы запретить прыжки в воздухе и убрать читерские двойные прыжки
CЧтобы цыплёнок падал быстрее
DЭто нужно только для рисования
6. Что произойдёт, если цыплёнок падает слишком быстро над тонкой платформой?
AОн отскочит вверх
BЗа один кадр он может «прошить» доску насквозь, и условие приземления не сработает
CПлатформа станет толще
DГравитация отключится