Отскок от краёв

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

Главное правило урока: чтобы объект отскакивал от стенки, надо проверить условием if, доехал ли он до края, и развернуть его движение, поменяв знак скорости: speed = -speed. Скорость хранится в переменной состояния и каждый кадр прибавляется к координате.

Зачем тебе отскок

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

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

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

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

Скорость — это шаг за один кадр

Что мы помним про движение

Прежде чем добавить отскок, освежим базу. Чтобы что-то двигалось, нам нужны две переменные состояния: где объект сейчас (координата) и как быстро он движется (скорость). Каждый кадр мы прибавляем скорость к координате — и объект сдвигается. Представь флипбук: на каждой странице цыплёнок чуть правее, чем на прошлой. Листаешь быстро — он едет.

let x = 50;     // где цыплёнок сейчас
let speed = 3;  // на сколько он сдвигается за кадр

function setup() {
  createCanvas(400, 400);
  noStroke();
}

function draw() {
  background(135, 206, 235);
  x = x + speed;        // двигаем вправо
  fill(255, 209, 64);
  circle(x, 200, 60);   // тело цыплёнка
}

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

Вот эта строка x = x + speed — мотор всей анимации. Скорость 3 означает «три пикселя вправо за кадр». Сделай её 1 — цыплёнок поползёт еле-еле; поставь 10 — рванёт пулей. Поиграй с числом и почувствуй, как оно управляет темпом. А теперь главный вопрос: как поймать момент, когда он дошёл до стенки?

Ловим край условием if

Где находится правая стенка

Холст у нас шириной 400 пикселей. Значит, правый край — это x = 400, левый — x = 0. Координата x цыплёнка растёт каждый кадр, и в какой-то момент она дотянется до 400. Именно этот момент нам надо поймать — проверкой через if: «если x стал больше или равен 400, значит, мы у правой стенки».

if (x > 400) {
  // цыплёнок дошёл до правого края — пора разворачиваться
}

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

Почему именно 400, а не, скажем, 380? Потому что мы условились мерить край по самой границе холста: его ширину мы задали в createCanvas(400, 400). Левый верхний угол холста — это точка (0, 0), как мы разбирали в самом начале курса, а правый нижний — (400, 400). Значит, любая координата x от 0 до 400 находится внутри холста, а всё, что меньше нуля или больше четырёхсот, — уже за его пределами. Держи эту картинку в голове: холст — как клетчатый лист в тетради, и мы просто проверяем, не вышел ли цыплёнок за поля. Кстати, вместо жёсткого числа 400 опытные авторы пишут width — это встроенная переменная p5.js, в которой хранится ширина холста. Тогда, если ты поменяешь размер холста, проверки подстроятся сами. Но для первого раза число нагляднее, поэтому пока оставим 400.

Разворот через смену знака

Теперь самое красивое. Чтобы цыплёнок поехал обратно, не нужно ничего сложного — достаточно поменять знак скорости. Была скорость +3 (вправо) — станет -3 (влево). Ведь если прибавлять отрицательное число, координата начнёт уменьшаться, и объект поедет в обратную сторону.

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

speed = -speed;

Если speed был 3, после этой строки он станет -3. А если был -3 — снова станет 3 (минус на минус даёт плюс). Поэтому одна и та же строка работает и у правой, и у левой стенки: она просто переворачивает направление, какое бы оно ни было сейчас.

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

Собираем прыгающего цыплёнка

Соединим всё вместе. Будем двигать цыплёнка вправо, а у каждой из двух стенок — разворачивать. Проверим оба края: правый (x > 400) и левый (x < 0).

let x = 200;
let speed = 4;

function setup() {
  createCanvas(400, 400);
  noStroke();
}

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

  x = x + speed;            // двигаем

  if (x > 400 || x < 0) {  // достигли любого края?
    speed = -speed;          // разворачиваемся
  }

  fill(255, 209, 64);       // тело
  circle(x, 200, 60);
  fill(255, 140, 30);       // клюв
  triangle(x, 195, x + 35, 200, x, 215);
}

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

Разберём по строкам, что происходит каждый кадр:

  1. background(...) — закрашиваем небо заново, стирая прошлый кадр.
  2. x = x + speed — сдвигаем цыплёнка на величину скорости.
  3. if (x > 400 || x < 0) — проверяем сразу обе стенки. Значок || читается как «или»: условие сработает, если истинно хотя бы одно из двух.
  4. speed = -speed — если у любой стенки, переворачиваем направление.
  5. рисуем тело и клюв в текущей точке x.

Обрати внимание: проверку мы делаем после того, как сдвинули цыплёнка, но до того, как нарисовали. Так разворот успевает сработать в том же кадре, и ты не увидишь, как герой залезает за край.

Прыжок по вертикали и по диагонали

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

let x = 200, y = 200;
let sx = 3, sy = 2;   // скорости по x и y

function setup() {
  createCanvas(400, 400);
  noStroke();
}

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

  x = x + sx;
  y = y + sy;

  if (x > 400 || x < 0) { sx = -sx; }  // боковые стенки
  if (y > 400 || y < 0) { sy = -sy; }  // верх и низ

  fill(255, 209, 64);
  circle(x, y, 60);
  fill(255, 140, 30);
  triangle(x, y - 5, x + 35, y, x, y + 15);
}

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

Тут мы завели две отдельные скорости — sx и sy — и проверяем каждую пару стенок своим if. Горизонталь и вертикаль живут независимо: цыплёнок может одновременно ехать вправо и вниз. Это ровно та же идея, что и раньше, просто продублированная для второй оси.

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

И ещё момент: скорости можно делать разными по величине. Если sx больше sy, цыплёнок будет лететь более полого, почти горизонтально, лишь слегка опускаясь. Если наоборот — траектория станет крутой, почти вертикальной. А если задать им одинаковые значения, получится идеальная диагональ под 45 градусов — та самая, при которой логотип DVD рано или поздно щёлкает точно в угол. Соотношение двух скоростей задаёт угол полёта, и, играя с ним, ты управляешь характером движения, не трогая саму логику отскока.

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

  • Дрожание у края (объект застрял в стенке). Если поставить условие if (x == 400), оно почти никогда не сработает: при скорости 4 координата перепрыгивает с 398 на 402, минуя ровно 400. А если случайно попадёт, при следующем кадре снова окажется за краем — и цыплёнок начнёт нервно дрожать у стенки. Поэтому всегда сравнивай через > и <, а не через ==: «зашёл за край или дальше», а не «попал точно в точку».

  • Скорость растёт сама собой. Если вместо speed = -speed написать speed = -speed - 1 или прибавлять к ней что-то при каждом отскоке, цыплёнок будет разгоняться с каждым ударом и в итоге начнёт телепортироваться через весь экран. При обычном отскоке скорость по модулю не меняется — меняется только знак.

  • Забыл проверить вторую стенку. Очень частая ошибка: написал if (x > 400) и забыл про x < 0. Тогда цыплёнок отскочит от правой стенки, доедет до левой и спокойно улетит за неё навсегда. Нужны обе проверки — либо через || в одном условии, либо двумя отдельными if.

  • Учёл только центр, а не радиус. Координата x — это центр круга. При диаметре 60 радиус равен 30, поэтому правый бок цыплёнка касается стенки уже при x = 370, а не при 400. Если хочешь, чтобы отскок шёл точно по краю фигуры, проверяй x > 400 - 30 и x < 30. Для начала можно этим пренебречь, но знай, откуда «утопание» в стенку.

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

Мини-практика: пинбол с CodeChick

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

  1. Сделай так, чтобы при каждом отскоке от стенки цыплёнок менял цвет тела. Заведи переменные для трёх компонент цвета и при срабатывании if присваивай им новые значения — например, через random(255). Получится дискотека.
  2. Учти радиус цыплёнка в проверках, чтобы он отскакивал ровно своим боком, а не центром. Замени 400 на 400 - 30, а 0 на 30.
  3. Добавь на холст неподвижное зерно (маленький круг) и подсчитывай в переменной, сколько раз цыплёнок отскочил. Выведи счётчик на экран функцией text() — как очки в игре.

Меняй стартовые скорости sx и sy и смотри, как перестраивается траектория. Поставь обе равными — получишь идеальную диагональ под 45 градусов. Сделай одну большой, другую маленькой — получишь пологий, почти горизонтальный полёт.

Итоги

Сегодня ты сделал свою первую самостоятельно живущую сцену. Запомни главное:

  • Скорость объекта хранится в переменной состояния и каждый кадр прибавляется к координате: x = x + speed.
  • Достижение края ловят условием if через > и < (не через ==) — проверяя обе стенки.
  • Разворот — это смена знака скорости: speed = -speed. Одна строка работает у любой стенки.
  • Для движения по диагонали заводят отдельные скорости по x и y и проверяют каждую пару стенок своим условием.

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

Проверьте себя
1. Какой код разворачивает движение объекта в обратную сторону?
Aspeed = speed + 1
Bspeed = -speed
Cspeed = 0
Dx = -x
2. Почему для проверки края лучше писать if (x > 400), а не if (x == 400)?
AТак короче писать
B== не работает с числами в p5.js
CПри большой скорости координата перепрыгивает точное значение 400, и проверка на равенство почти никогда не сработает
D> работает быстрее, чем ==
3. Что произойдёт, если проверить только правую стенку (x > 400) и забыть про левую (x < 0)?
AЦыплёнок будет дрожать у правого края
BЦыплёнок отскочит от правой стенки, доедет до левой и улетит за неё навсегда
CНичего, p5.js сам добавит вторую проверку
DСкетч выдаст ошибку
4. Зачем для движения по диагонали заводят две скорости — sx и sy?
AДля красоты, можно обойтись одной
BЧтобы объект двигался независимо по горизонтали и по вертикали, отскакивая от своих пар стенок
CЧтобы скетч работал быстрее
DЭто требование функции circle()
5. В какой момент кадра правильнее всего проверять достижение края?
AПосле того как сдвинули объект, но до того как нарисовали его
BВ самом начале, до движения
CПосле того как нарисовали объект
DТолько в setup(), один раз
6. Что не так с кодом отскока speed = -speed - 1?
AНичего, это правильный отскок
BСкорость будет расти с каждым ударом, и объект начнёт телепортироваться через экран
CОбъект остановится
DЗнак не поменяется