p5.Vector: позиция и скорость

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

Зачем тебе вектор

Вспомни прошлый урок про переменные состояния. Чтобы цыплёнок ехал по холсту, ты завёл четыре переменные: x, y для позиции и speedX, speedY для скорости. В draw() ты прибавлял скорость к позиции — отдельно по иксу, отдельно по игреку. Работало! Но переменных стало многовато, и они норовили разъехаться: поменял имя одной, забыл вторую — и цыплёнок едет боком.

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

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

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

Что такое вектор: стрелка от носа цыплёнка

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

В нашем курсе вектор удобнее всего представлять двумя способами, и оба правильные.

  • Вектор-позиция. Стрелка из левого верхнего угла холста (точки 0, 0) прямо в цыплёнка. Она говорит: «герой вот здесь». Числа (x, y) — это его адрес на холсте, ровно как клетка в тетради по столбцу и строке.
  • Вектор-скорость. Стрелка из носа цыплёнка туда, куда он шагнёт в следующем кадре. Если вектор равен (3, 0) — за кадр цыплёнок сдвинется на 3 пикселя вправо и ни на сколько вниз. Если (-2, 5) — на 2 влево и 5 вниз.

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

Запомни фразу-заклинание: позиция += скорость. Почти вся анимация в этом курсе, от прыгающего мячика до фейерверка, держится на этой одной строчке. Вектор просто делает её аккуратной.

Создаём вектор: createVector()

Чтобы получить вектор, не надо ничего импортировать — в p5.js есть готовая функция-фабрика createVector(x, y). Передаёшь ей пару чисел, она возвращает объект-вектор.

let pos = createVector(200, 150);

console.log(pos.x); // 200
console.log(pos.y); // 150

Результат: в переменной pos теперь лежит вектор, который помнит сразу два числа. Достать их по отдельности можно через точку: pos.x вернёт 200, pos.y — 150. Точка здесь читается как «возьми у вектора его икс».

Видишь разницу с прошлым уроком? Раньше было две независимые переменные x и y. Теперь одна переменная pos, а внутри неё аккуратно лежат .x и .y. Они всегда вместе, всегда заодно — ты физически не сможешь забыть одну из них при передаче, потому что таскаешь их одним объектом.

Разбираем на примерах

Пример 1. Ставим цыплёнка по вектору-позиции

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

let pos;

function setup() {
  createCanvas(400, 400);
  pos = createVector(200, 200); // центр холста
}

function draw() {
  background(135, 206, 235); // небо
  noStroke();

  fill(255, 221, 51);       // жёлтое тело
  circle(pos.x, pos.y, 80);

  fill(255, 140, 0);        // оранжевый клюв
  triangle(pos.x + 35, pos.y, pos.x + 55, pos.y - 6, pos.x + 55, pos.y + 6);
}

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

Разберём по шагам. Переменную pos мы объявили снаружи функций — чтобы она была видна и в setup(), и в draw(). В setup() один раз создаём вектор в центре. А в draw() рисуем тело и клюв, привязав все координаты к pos.x и pos.y. Обрати внимание: клюв нарисован относительно центра (pos.x + 35 и так далее). Это важная привычка — когда герой собран вокруг одной точки, его легко двигать целиком. Сдвинешь pos — и тело, и клюв уедут вместе, как приклеенные.

Пример 2. Оживляем: pos.add(vel)

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

let pos;
let vel;

function setup() {
  createCanvas(400, 400);
  pos = createVector(50, 200);  // старт слева
  vel = createVector(3, 0);     // 3 пикселя вправо за кадр
}

function draw() {
  background(135, 206, 235);
  noStroke();

  pos.add(vel); // ГЛАВНАЯ СТРОКА: позиция += скорость

  fill(255, 221, 51);
  circle(pos.x, pos.y, 80);
  fill(255, 140, 0);
  triangle(pos.x + 35, pos.y, pos.x + 55, pos.y - 6, pos.x + 55, pos.y + 6);
}

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

Сердце этого скетча — строка pos.add(vel). Читается она буквально: «вектор pos, прибавь к себе вектор vel». Под капотом p5.js делает то, что ты в прошлом уроке писал руками в две строки: складывает иксы (pos.x += vel.x) и игреки (pos.y += vel.y). Но теперь это одна команда, которая работает сразу с обеими координатами и никогда не забудет одну из них.

Метод .add() меняет сам вектор pos — после вызова в нём уже новые координаты. Поэтому на каждом кадре draw() позиция подрастает на скорость, и цыплёнок едет. Попробуй мысленно поменять vel на createVector(3, 2) — и герой поедет не строго вправо, а по диагонали, вправо-вниз. Хочешь резвее? Сделай createVector(8, 0) — рванёт быстрее. Скорость — это просто стрелка, а ты задаёшь её длину и наклон.

Пример 3. Отскок от стен: цыплёнок в коробке

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

let pos;
let vel;
let r = 40; // радиус тела цыплёнка

function setup() {
  createCanvas(400, 400);
  pos = createVector(200, 200);
  vel = createVector(3, 2); // едет по диагонали
}

function draw() {
  background(135, 206, 235);
  noStroke();

  pos.add(vel);

  // отскок по горизонтали
  if (pos.x > width - r || pos.x < r) {
    vel.x = vel.x * -1;
  }
  // отскок по вертикали
  if (pos.y > height - r || pos.y < r) {
    vel.y = vel.y * -1;
  }

  fill(255, 221, 51);
  circle(pos.x, pos.y, r * 2);
}

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

Логика отскока стоит того, чтобы её прочувствовать. Проверка pos.x > width - r ловит момент, когда правый бок цыплёнка дотронулся до правой стены. Мы вычитаем радиус r, чтобы разворот случился, когда край тела касается стены, а не центр пролетает сквозь неё. Если коснулся — пишем vel.x = vel.x * -1, то есть меняем знак иксовой скорости: ехал вправо (+3) — теперь едет влево (−3). Игрек при этом не трогаем, поэтому вертикальное движение продолжается как ни в чём не бывало. Точно так же отдельная проверка следит за верхом и низом через vel.y. Две независимые оси — два независимых отскока, и вместе они дают честную физику бильярдного шара.

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

Векторы простые, но первые скетчи на них почти всегда спотыкаются об одни и те же грабли. Лови их заранее.

1. Создают вектор в draw() вместо setup()

Самая частая беда. Человек пишет let pos = createVector(50, 200) прямо внутри draw().

function draw() {
  let pos = createVector(50, 200); // создаётся ЗАНОВО каждый кадр
  pos.add(vel);
  circle(pos.x, pos.y, 80);
}

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

2. Путают pos.add(vel) и pos = pos.add(vel)

Метод .add() и так меняет сам вектор pos прямо на месте. Если по привычке написать pos = pos.add(vel), в большинстве случаев ничего страшно не сломается, но это лишнее и сбивает с толку: кажется, будто add возвращает новый вектор, а он меняет старый. Пиши просто pos.add(vel) — одной командой, как поворот ключа.

3. Забывают точку: pos вместо pos.x

Внутри circle() или fill() нужны числа, а вектор — это объект из пары чисел. Если написать circle(pos, 80) вместо circle(pos.x, pos.y, 80), p5.js не поймёт, где икс, а где игрек, и фигура либо не нарисуется, либо встанет в угол (0, 0). Запомни: вектор хранишь целиком, а вот рисуешь по его частям — pos.x и pos.y.

4. Меняют не ту ось при отскоке

При отскоке от боковой стены разворачивать надо vel.x, а от пола или потолкаvel.y. Новички часто путают и пишут vel.y в горизонтальной проверке. Тогда цыплёнок отскакивает как-то странно — будто стены наклонены. Простое правило: левая и правая стены отвечают за икс, верхняя и нижняя — за игрек.

5. Цыплёнок «прилипает» к стене и трясётся

Если забыть про радиус и проверять только pos.x > width, бывает, что герой заезжает за край, разворачивается, на следующем кадре всё ещё за краем — снова разворачивается, и так залипает у стенки, мелко дрожа. Лекарство — проверять касание края тела (с учётом радиуса r), как в нашем примере, чтобы разворот случался вовремя, до того как цыплёнок утопает в стене.

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

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

  1. Сделай цыплёнка узнаваемым: жёлтое тело-круг, чуть меньший круг-голову сверху и оранжевый клюв-треугольник. Привяжи все части к одной точке pos, чтобы при движении он не разваливался.
  2. Задай старт в центре, а скорость — диагональную, например createVector(4, 3). Запусти и убедись, что цыплёнок носится по всему холсту и отскакивает от всех четырёх стен.
  3. Случайный старт. Вспомни random(): задай скорость как createVector(random(-5, 5), random(-5, 5)). Теперь при каждом запуске цыплёнок летит в новую сторону.
  4. Бонус — глаз по ходу движения. Рисуй глаз чуть смещённым в сторону, куда указывает скорость: если vel.x > 0 — глаз правее, иначе левее. Герой будет «смотреть» туда, куда летит.
  5. Супербонус — гнездо. Нарисуй в углу маленькое неподвижное гнездо и придумай, что должно произойти, когда цыплёнок до него долетит (например, смени фон или ускорь героя). Это уже шаг к настоящей игре.

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

Итоги

Сегодня ты переписал движение цыплёнка на векторах. Главное:

  • Вектор — это пара чисел (x, y) с понятным смыслом «куда и насколько»: позиция героя или его скорость.
  • Создаётся вектор через createVector(x, y), один раз и обычно в setup(). Отдельные числа достаёшь через точку: pos.x, pos.y.
  • Движение — это pos.add(vel): «позиция += скорость». Одна строка вместо двух, и она читается как фраза.
  • Отскок — это смена знака нужной оси скорости: боковые стены трогают vel.x, пол и потолок — vel.y.
  • Вектор спасает, когда героев становится много: вместо россыпи переменных у каждого аккуратная пара pos и vel.

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

Проверьте себя
1. Что хранит объект p5.Vector?
AПару чисел (x, y) как единое целое — например, позицию или скорость
BТолько одно число
CЦвет фигуры в формате RGB
DТекстовую подпись для фигуры
2. Как правильно создать вектор с координатами (200, 150)?
AcreateVector(200, 150)
BnewVector(200, 150)
Cvector(200, 150)
DcreatePoint(200, 150)
3. Что делает строка pos.add(vel) в draw()?
AПрибавляет скорость к позиции, сдвигая героя на один шаг
BСоздаёт новый пустой вектор
CОстанавливает движение героя
DМеняет цвет цыплёнка
4. Цыплёнок застыл на старте и не двигается, хотя есть pos.add(vel). В чём типичная причина?
AВектор pos создаётся заново в draw() каждый кадр вместо setup()
BСкорость задана слишком большой
CЗабыли вызвать background()
DХолст слишком маленький
5. Цыплёнок коснулся ПРАВОЙ стены. Какую ось скорости надо развернуть, чтобы он отскочил?
Avel.x — поменять её знак на минус
Bvel.y — поменять её знак на минус
Cобе оси сразу
Dни одну, скорость менять нельзя
6. Почему внутри circle() пишут circle(pos.x, pos.y, 80), а не circle(pos, 80)?
Acircle() ждёт отдельные числа, а pos — это объект из пары чисел
Bpos.x работает быстрее, чем pos
Ccircle() вообще не принимает векторы и переменные
Dтак короче писать