Переменные состояния: двигаем объект

Чтобы объект поехал по холсту, ему нужна память — переменная, которая хранит его координату между кадрами и капельку меняется в каждом новом.

Переменная состояния — это переменная, которая хранит данные между кадрами (например, координату объекта) и меняется в draw(). Именно из этой «памяти» рождается движение.

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

Зачем это вообще нужно

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

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

Подумай про любую игру в твоём телефоне. Птица в Flappy Bird, шарик в Brawl Stars, твой персонаж в Minecraft — у каждого есть координаты, которые игра хранит и чуть-чуть меняет каждый кадр. Нажал вправо — программа прибавила к координате X несколько пикселей, перерисовала кадр, и тебе кажется, что персонаж поехал. На самом деле он не «едет» — он просто рисуется каждый раз в новом месте, а место хранится в переменной.

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

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

Память между кадрами: что такое переменная состояния

Давай разберёмся, почему обычная переменная внутри draw() для движения не годится. Каждый раз, когда p5.js вызывает draw(), всё, что объявлено внутри этой функции, создаётся заново с нуля. Это как если бы художник на каждой странице флипбука доставал новый чистый лист и забывал всё, что рисовал до этого. Координата, объявленная внутри draw(), будет сбрасываться к стартовому значению в каждом кадре — и цыплёнок навечно застынет в одной точке.

Значит, координату надо хранить снаружи draw() — на уровне всего скетча. Тогда она переживает один кадр и достаётся следующему. Это и называется переменной состояния: она хранит «состояние мира» (где сейчас наш герой) и живёт между кадрами, а не внутри одного.

Рецепт движения всегда состоит из трёх шагов, и его стоит просто запомнить:

  1. Объяви переменную снаружи setup() и draw() — там она будет жить весь скетч.
  2. Используй её внутри draw(), чтобы нарисовать объект в текущей позиции.
  3. Измени её в конце draw() — прибавь чуть-чуть, чтобы в следующем кадре объект оказался в новом месте.

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

Пример 1. Цыплёнок едет вправо

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

let x = 50; // переменная состояния: горизонтальная позиция цыплёнка

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

function draw() {
  background(230, 245, 255); // перерисовываем небо — стираем прошлый кадр

  // рисуем цыплёнка в текущей позиции x
  noStroke();
  fill(255, 220, 60);
  circle(x, 200, 80);      // тело
  fill(245, 130, 20);
  triangle(x + 30, 195, x + 55, 200, x + 30, 210); // клюв

  x = x + 2; // двигаем: в следующем кадре цыплёнок будет правее
}

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

Разберём по нашему рецепту:

  1. let x = 50; стоит в самом верху, до setup(). Значит, x — переменная состояния: она создаётся один раз и живёт весь скетч. Стартует с 50 — это положение у левого края.
  2. Внутри draw() мы рисуем тело и клюв, используя x как координату. Заметь: клюв нарисован относительно xx + 30, x + 55 — чтобы он ехал вместе с телом, а не отваливался.
  3. Последняя строка x = x + 2; — сердце анимации. Она читается так: «возьми текущее x, прибавь 2 и запиши обратно в x». В следующем кадре x уже будет 52, потом 54, 56... и цыплёнок ползёт вправо.

И ещё одна критически важная строка — background(...) в начале draw(). Она каждый кадр заново заливает холст небом и тем самым стирает прошлое положение цыплёнка. Если её убрать, старые цыплята не сотрутся, и за героем потянется сплошной жёлтый след — будто он размазался по экрану. Иногда такой след делают нарочно (это красиво!), но для чистого движения фон нужно перерисовывать.

Пример 2. Цыплёнок-циркуляр: доехал до края — вернулся

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

let x = -40; // стартуем чуть за левым краем

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

function draw() {
  background(230, 245, 255);

  noStroke();
  fill(255, 220, 60);
  circle(x, 200, 80);
  fill(245, 130, 20);
  triangle(x + 30, 195, x + 55, 200, x + 30, 210);

  x = x + 2;

  // если цыплёнок полностью уехал за правый край — вернуть налево
  if (x > 440) {
    x = -40;
  }
}

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

Новое здесь — проверка if (x > 440). Холст шириной 400, и пока x не превысит 440, цыплёнок ещё частично виден. Как только x переваливает за 440 — он полностью скрылся, и мы возвращаем x = -40 (чуть левее холста, чтобы он выехал плавно, а не выпрыгнул из ниоткуда). Эта маленькая проверка превращает разовый проезд в вечный цикл.

Поиграй с числами: поставь x = x + 5 — цыплёнок помчится быстрее. Поставь + 0.5 — поползёт еле-еле. Скорость движения — это и есть размер шага, который ты прибавляешь каждый кадр. Поменяй число и посмотри, что будет.

Пример 3. Две переменные — движение по диагонали

Одна переменная двигает по горизонтали. Добавим вторую — y — и цыплёнок поедет по диагонали, отскакивая от пола и потолка, как мяч в старом скринсейвере DVD (тот самый логотип, который все ждали, попадёт ли в угол).

let x = 60;
let y = 60;
let speedX = 3; // шаг по горизонтали
let speedY = 2; // шаг по вертикали

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

function draw() {
  background(230, 245, 255);

  noStroke();
  fill(255, 220, 60);
  circle(x, y, 70);

  x = x + speedX;
  y = y + speedY;

  // отскок от левой/правой стенки
  if (x > 365 || x < 35) {
    speedX = -speedX;
  }
  // отскок от пола/потолка
  if (y > 365 || y < 35) {
    speedY = -speedY;
  }
}

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

Здесь уже четыре переменные состояния: позиция (x, y) и скорость (speedX, speedY). Скорость — тоже состояние: мы её храним и меняем. Трюк с отскоком прост и красив: чтобы развернуть движение, мы меняем знак скорости. speedX = -speedX превращает «плюс три» в «минус три» — и цыплёнок, который ехал вправо, поедет влево. Никаких сложных формул: просто разворачиваем шаг в обратную сторону, когда упёрлись в стенку.

Обрати внимание на числа 35 и 365: радиус цыплёнка примерно 35 (диаметр 70 пополам), поэтому стенку он «чувствует» не в нуле, а отступив на свой радиус — иначе половина тела вылезала бы за край перед разворотом.

Ещё одна тонкость — почему в условии стоит x > 365 || x < 35 (два знака || читаются как «или»). Нам нужно развернуть цыплёнка и у правой стенки, и у левой. Если проверять только правую, он один раз отскочит вправо, уедет влево и навсегда улетит за левый край. Поэтому мы проверяем обе границы сразу: «уперся справа или уперся слева — разворачивай». То же самое по вертикали для пола и потолка. Четыре стенки — два условия, и цыплёнок заперт внутри холста, как шарик в коробке.

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

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

На этих граблях спотыкаются почти все, кто впервые двигает объект кодом. Узнаешь их заранее — сэкономишь себе вечер «почему оно не едет».

1. Переменная объявлена внутри draw()

Самая частая ошибка. Если написать let x = 50; внутри draw(), то x будет заново становиться 50 в каждом кадре. Ты прибавляешь 2, получаешь 52 — но в следующем кадре переменная опять рождается равной 50. Цыплёнок дёргается на месте и никуда не едет. Лекарство: объявляй переменную состояния снаружи, над setup().

2. Забыл перерисовать фон

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

3. Меняешь координату, но не прибавляешь, а присваиваешь

Легко перепутать x = x + 2 (прибавить к текущему) и, скажем, x = 2 (поставить ровно 2). Второе не двигает — оно каждый кадр насильно ставит цыплёнка в одну и ту же точку 2. Движение рождается именно из прибавления к прошлому значению: x = x + шаг. Существует и короткая запись того же самого — x += 2, она делает ровно то же, что x = x + 2.

4. Клюв (или глаз) едет отдельно от тела

Если тело нарисовать в x, а клюв — в фиксированной координате вроде 230, то тело поедет, а клюв останется висеть на месте. Цыплёнок «развалится». Все части объекта должны считаться относительно общей координаты: тело в x, клюв в x + 30 и так далее. Тогда они едут вместе как одно целое.

5. Объект мгновенно улетает за край

Если в каждом кадре прибавлять большое число, например x = x + 80, цыплёнок будет не ехать, а телепортироваться огромными скачками и почти сразу скроется. Анимация — это много маленьких шагов. Для плавного движения шаг обычно 1–5 пикселей за кадр; чем меньше шаг, тем медленнее и плавнее ход.

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

Теперь твоя очередь. Возьми пример 2 (цыплёнок едет и возвращается) за основу и доведи его до маленькой законченной сценки:

  1. Добавь цыплёнку глаз. Маленький тёмный круг, нарисованный относительно x — например, в circle(x + 12, 188, 12). Проверь, что глаз едет вместе с телом, а не отстаёт.
  2. Посади его на землю. Нарисуй внизу холста зелёную полосу-травку (прямоугольником rect(...)) и опусти цыплёнка пониже, чтобы он будто бежал по траве.
  3. Поиграй со скоростью. Вынеси шаг в отдельную переменную let speed = 2; и меняй x = x + speed;. Попробуй speed = 1, speed = 6 — почувствуй разницу между прогулкой и спринтом.

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

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

Итоги

Сегодня ты понял главный секрет всякой анимации — он держится на переменной состояния и трёх шагах:

  • Переменная состояния хранит данные (например, координату) между кадрами. Объявляй её снаружи setup() и draw(), чтобы она пережила один кадр и досталась следующему.
  • Рецепт движения: объяви переменную → нарисуй объект в текущей позиции → измени переменную в конце draw() (x = x + шаг).
  • Скорость — это размер шага, который ты прибавляешь каждый кадр. Маленький шаг — плавно и медленно, большой — быстро и рывками.
  • Фон в начале draw() стирает прошлый кадр. Без него за объектом тянется след.
  • Развернуть движение можно, поменяв знак скорости: speed = -speed. На этом построены отскоки от стенок.

Заметь, как вырос наш цыплёнок: ещё недавно он просто стоял неподвижной кучкой кружков, а теперь катается по холсту и отскакивает от краёв. Это уже не картинка — это поведение.

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

Проверьте себя
1. Где нужно объявить переменную, которая хранит координату объекта между кадрами?
AВнутри draw(), в самом начале
BВнутри setup(), после createCanvas
CСнаружи setup() и draw(), на уровне всего скетча
DВнутри любой функции — p5.js сам всё запомнит
2. Почему переменная, объявленная внутри draw(), не годится для движения?
AОна каждый кадр создаётся заново и сбрасывается к стартовому значению
Bp5.js запрещает объявлять переменные внутри draw()
CОна движется слишком быстро
DЕё нельзя прибавлять к числам
3. Что делает строка x = x + 2 в конце draw()?
AСтавит x ровно равным 2 в каждом кадре
BБерёт текущее x, прибавляет 2 и записывает обратно — в следующем кадре объект будет правее
CРисует объект на 2 пикселя больше
DУдаляет переменную x
4. Что произойдёт, если убрать background(...) из начала draw()?
AСкетч выдаст ошибку
BОбъект перестанет двигаться
CПрошлые кадры не сотрутся — за объектом потянется сплошной след
DОбъект станет прозрачным
5. Как заставить летающий объект отскочить от стенки и поехать в обратную сторону?
AОбнулить координату: x = 0
BПоменять знак скорости: speedX = -speedX
CВызвать noLoop()
DУвеличить скорость вдвое
6. Тело цыплёнка едет (нарисовано в x), а клюв висит на месте. Почему?
AКлюв нарисован в фиксированной координате, а не относительно x
BТреугольники в p5.js не умеют двигаться
CНужно объявить отдельную переменную для клюва
DЭто баг p5.js