Ускорение, гравитация, ветер

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

Зачем цыплёнку физика

Запусти в голове любой ролик, где что-то падает: телефон выскальзывает из руки, мяч летит с трибуны, персонаж в платформере спрыгивает с уступа. Заметил? Ничто не падает с постоянной скоростью. Сначала движение медленное, почти лениво, а потом всё быстрее и быстрее — у самой земли предмет уже свистит. Этот разгон и есть гравитация: она не задаёт скорость, она её постоянно наращивает.

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

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

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

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

Цепочка из трёх звеньев: ускорение → скорость → позиция

Представь, что ты едешь на велосипеде. У тебя есть три разные вещи, и их легко перепутать. Первое — где ты сейчас на дороге. Это позиция. Второе — как быстро и куда ты катишь прямо сейчас. Это скорость. Третье — давишь ли ты на педали или, наоборот, на тормоз. Это ускорение: оно не говорит, где ты и насколько быстро едешь, оно говорит, как меняется твоя скорость.

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

ВекторЧто хранитКто его меняет каждый кадр
pos (позиция)где цыплёнок на холстескорость vel
vel (скорость)куда и как быстро летитускорение acc
acc (ускорение)как меняется скоростьсилы (гравитация, ветер)

Каждый кадр в draw() ты делаешь два сложения, и обязательно в этом порядке: сначала ускорение прибавляешь к скорости, потом скорость прибавляешь к позиции. На векторах p5.js это выглядит так:

vel.add(acc); // скорость подрастает на величину ускорения
pos.add(vel); // позиция сдвигается на новую скорость

Вот и вся физика. Звучит почти обманчиво просто, но именно из этих двух строк рождаются падение, отскок, полёт по дуге и десятки других движений. Меняется только то, что ты записываешь в acc.

Почему гравитация — это вектор

Гравитация всегда тянет вниз. На холсте «вниз» — это рост игрека (вспомни: игрек растёт вниз). Значит, гравитацию удобно записать как вектор, у которого икс равен нулю (вбок она не тянет), а игрек — маленькое положительное число:

let gravity = createVector(0, 0.2);

Почему число такое крошечное — 0.2, а не, скажем, 20? Потому что ускорение прибавляется к скорости каждый кадр, а кадров за секунду — около шестидесяти. За первую секунду падения скорость накопит примерно 0.2 × 60 = 12 пикселей в кадр — этого уже достаточно, чтобы цыплёнок ощутимо разгонялся. Поставишь 20 — и он за пару кадров улетит сквозь нижний край с космической скоростью. Гравитация копится, поэтому числа берём маленькие.

Пример 1. Роняем цыплёнка

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

let pos;
let vel;
let gravity;

function setup() {
  createCanvas(400, 400);
  pos = createVector(200, 40);   // старт у верхнего края
  vel = createVector(0, 0);      // в покое, скорости нет
  gravity = createVector(0, 0.2); // тянет вниз
}

function draw() {
  background(30);

  // 1) ускорение (гравитация) добавляем к скорости
  vel.add(gravity);
  // 2) скорость добавляем к позиции
  pos.add(vel);

  // рисуем цыплёнка в текущей позиции
  noStroke();
  fill(255, 221, 51);
  circle(pos.x, pos.y, 60);       // тело
  fill(255, 140, 0);
  triangle(pos.x + 22, pos.y - 4, pos.x + 22, pos.y + 6, pos.x + 40, pos.y + 1); // клюв
  fill(30);
  circle(pos.x + 12, pos.y - 10, 7); // глаз
}

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

Разберём по шагам, что происходит. В setup() мы один раз задаём три вектора: позицию вверху, нулевую скорость (цыплёнок в покое) и гравитацию. В draw() каждый кадр выполняется та самая цепочка: vel.add(gravity) чуть-чуть увеличивает скорость вниз, затем pos.add(vel) сдвигает цыплёнка на эту новую скорость. На первом кадре vel станет (0, 0.2), на втором (0, 0.4), на третьем (0, 0.6) — скорость растёт линейно, а позиция поэтому едет всё дальше за кадр. Так выглядит свободное падение.

Поиграй с числами

  • Сила тяжести. Поменяй 0.2 на 0.05 — цыплёнок будет падать медленно и мягко, словно на Луне. Поставь 0.6 — рухнет почти мгновенно.
  • Толчок вверх. Задай стартовую скорость vel = createVector(0, -8). Цыплёнок сначала подпрыгнет вверх, замедлится, на миг зависнет и полетит вниз — гравитоция переборет толчок. Это уже траектория прыжка!
  • Боковой разбег. Дай немного скорости вбок: vel = createVector(3, 0). Цыплёнок полетит по красивой дуге — вправо и вниз одновременно, как мяч, брошенный с балкона.

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

Гравитация — не единственная сила в мире. Добавим ветер: он дует вбок, то есть это вектор, у которого ненулевой икс. И вот здесь раскрывается главная красота векторов — силы складываются. Чтобы получить общее ускорение, мы просто сложим гравитацию и ветер в один вектор acc.

let pos;
let vel;
let gravity;
let wind;

function setup() {
  createCanvas(400, 400);
  pos = createVector(80, 40);
  vel = createVector(0, 0);
  gravity = createVector(0, 0.2);  // вниз
  wind = createVector(0.1, 0);     // слабый ветер вправо
}

function draw() {
  background(30);

  // собираем общее ускорение из всех сил
  let acc = createVector(0, 0);
  acc.add(gravity);
  acc.add(wind);

  // та же цепочка: acc -> vel -> pos
  vel.add(acc);
  pos.add(vel);

  noStroke();
  fill(255, 221, 51);
  circle(pos.x, pos.y, 60);
  fill(255, 140, 0);
  triangle(pos.x + 22, pos.y - 4, pos.x + 22, pos.y + 6, pos.x + 40, pos.y + 1);
  fill(30);
  circle(pos.x + 12, pos.y - 10, 7);
}

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

Главное, что нужно увидеть в этом примере: мы не придумывали никакой особой логики для «гравитации с ветром». Мы просто сложили два вектора-силы в один acc и пропустили его через ту же самую цепочку vel.add(acc); pos.add(vel). Захочешь добавить третью силу — например, тягу к центру холста — просто прибавишь ещё один acc.add(...). Векторы складываются как угодно, и каждый кадр их сумма решает, куда полетит цыплёнок.

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

Почему acc обнуляется каждый кадр

Обрати внимание: acc мы создаём заново внутри draw(), на каждом кадре с нуля. Это важно. Ускорение — это не запас, который копится, а команда «вот так толкаем прямо сейчас». Толчок длится один кадр, потом всё начинается заново. Копится у нас скорость (vel живёт в setup() и накапливает изменения), а ускорение — мгновенное, поэтому его мы каждый кадр собираем свежим из текущих сил. Если этого не делать, силы будут наслаиваться сами на себя и цыплёнок взбесится — об этой ловушке поговорим ниже.

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

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

1. Перепутан порядок сложения

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

2. Слишком большая гравитация

Поставил createVector(0, 10) вместо 0.2 — и цыплёнок исчезает за один кадр: скорость за пару кадров вырастает до сотен пикселей, и он телепортируется сквозь холст. Ускорение прибавляется десятки раз в секунду, поэтому числа должны быть маленькими, дробными. Если движение «слишком быстрое и резкое» — первым делом уменьшай ускорение.

3. acc забыли обнулить и он копится

Если завести acc один раз в setup() и каждый кадр делать acc.add(gravity), ускорение начнёт само себя накапливать: 0.2, потом 0.4, 0.6, 0.8… Цыплёнок будет разгоняться неестественно быстро, по нарастающей. Лекарство простое: либо создавай acc заново каждый кадр (как в примере 2), либо в конце кадра обнуляй его acc.mult(0). Ускорение — мгновенный толчок, а не копилка.

4. Гравитацию записали в скорость

Иногда вместо того чтобы прибавлять гравитацию к скорости, новичок просто задаёт vel = createVector(0, 0.2) и думает, что это падение. Но так цыплёнок будет ехать вниз с постоянной скоростью 0.2 — медленно и без разгона, как лифт. Весь смысл ускорения в том, что оно прибавляется к скорости каждый кадр и заставляет её расти. Падение без разгона — это не падение, а равномерное сползание.

5. Изменили вектор там, где не ждали

Методы вроде vel.add(acc) меняют сам вектор vel на месте, а не создают новый. Это удобно, но если ты случайно вызовешь gravity.add(wind), ты испортишь сам вектор гравитации навсегда — на следующем кадре в нём окажется уже сумма. Поэтому силы складывают в отдельный acc, а исходные векторы gravity и wind держат нетронутыми.

Мини-проект: цыплёнок в порыве ветра

Пора собрать всё своими руками. Возьми пример с ветром за основу и доработай его по чек-листу:

  1. Урони цыплёнка из верхнего левого угла под действием гравитации createVector(0, 0.2) — добейся, чтобы он падал с разгоном.
  2. Добавь ветер вправо createVector(0.1, 0) и убедись, что траектория стала диагональной дугой, а не вертикальной линией.
  3. Сделай ветер сильнее и слабее, поменяй его направление на влево (отрицательный икс). Посмотри, как меняется наклон дуги.
  4. Когда цыплёнок улетает за нижний край, верни его наверх: если pos.y > height, задай pos.y = 0 и обнули скорость vel = createVector(0, 0). Получится бесконечный «листопад» из одного цыплёнка.
  5. Бонус. Сделай ветер порывистым: каждый кадр задавай wind = createVector(random(-0.15, 0.15), 0). Цыплёнок начнёт болтаться из стороны в сторону, пока падает — как настоящее пёрышко на ветру.

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

Итоги

Сегодня ты дал цыплёнку настоящую физику и понял, что под капотом она устроена на удивление просто. Коротко главное:

  • В движении три звена: ускорение меняет скорость, скорость меняет позицию. Каждый кадр — два сложения: vel.add(acc), потом pos.add(vel), именно в таком порядке.
  • Гравитация — это вектор вниз с маленьким игреком (createVector(0, 0.2)). Числа крошечные, потому что ускорение прибавляется десятки раз в секунду и копится в скорости.
  • Силы складываются: чтобы добавить ветер, просто прибавь его вектор к acc. Хоть три силы, хоть пять — сумма решает, куда полетит герой.
  • acc собирают заново каждый кадр (или обнуляют), иначе ускорение накапливает само себя и движение взбесится. Копится скорость, а не ускорение.

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

Проверьте себя
1. В цепочке движения какой вектор каждый кадр меняет позицию (pos)?
AСкорость (vel)
BУскорение (acc)
CГравитация напрямую
DПозиция меняет сама себя
2. Почему гравитацию задают маленьким числом, например createVector(0, 0.2), а не createVector(0, 20)?
AУскорение прибавляется к скорости каждый кадр (десятки раз в секунду) и быстро накапливается
Bp5.js не умеет работать с большими числами
CИначе цыплёнок будет падать вверх
DБольшие числа замедляют программу
3. В каком порядке нужно выполнять сложения каждый кадр?
AСначала vel.add(acc), потом pos.add(vel)
BСначала pos.add(vel), потом vel.add(acc)
CПорядок не важен, результат одинаковый
DСначала acc.add(pos), потом vel.add(acc)
4. Как из гравитации и ветра получить общее ускорение acc?
AСложить оба вектора-силы: acc.add(gravity); acc.add(wind)
BПеремножить векторы: acc = gravity.mult(wind)
CВзять только тот, который больше
DСилы нельзя комбинировать, нужно выбрать одну
5. Что произойдёт, если завести acc один раз в setup() и каждый кадр делать acc.add(gravity), не обнуляя его?
AУскорение начнёт накапливать само себя (0.2, 0.4, 0.6...) и цыплёнок разгонится неестественно быстро
BНичего не изменится, движение будет правильным
CЦыплёнок остановится
DГравитация исчезнет
6. Новичок написал vel = createVector(0, 0.2) и не прибавляет гравитацию к скорости. Как полетит цыплёнок?
AПоедет вниз с постоянной скоростью, без разгона — как лифт
BБудет падать с ускорением, как при настоящей гравитации
CПолетит вверх
DОстанется на месте