Клавиатура: управление с клавиш

Сегодня CodeChick наконец перестанет просто стоять и начнёт слушаться твоих клавиш — стрелок и букв, как настоящий герой видеоигры.

Главная идея урока: у p5.js есть два способа узнать про клавиши. keyPressed() срабатывает один раз в момент нажатия — это про разовые действия. А keyIsDown() проверяет прямо сейчас, зажата ли клавиша — это про плавное движение, пока ты держишь стрелку.

Зачем тебе клавиатура

Вспомни любую игру, в которую ты залипал: Mario, Minecraft, какой-нибудь платформер на телефоне с раскладкой кнопок на экране. Что в них главное? Ты жмёшь стрелку — и герой идёт. Отпускаешь — он встаёт. Жмёшь пробел — он прыгает. Между твоим пальцем и персонажем будто протянута невидимая нить: ты двигаешь — он реагирует. Именно это ощущение управления и превращает картинку на экране в игру. Без него на экране — мультик, который крутится сам по себе и тебя не замечает. С ним — мир, который слушается тебя.

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

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

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

Как p5.js слышит клавиши

Метафора: дверной звонок и зажатая кнопка

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

В p5.js ровно эти две модели:

keyPressed() — это дверной звонок. Особая функция, которую p5.js вызывает один раз в тот момент, когда ты нажал любую клавишу. Идеально для разовых действий: прыгнуть, выстрелить, поменять цвет, поставить паузу.

keyIsDown(код) — это педаль газа. Функция, которая отвечает true, если клавиша с таким кодом зажата прямо в этот кадр, и false, если нет. Вызываешь её внутри draw() каждый кадр — и пока стрелка зажата, герой едет.

Запомни это разделение, оно — сердце всего урока. Разовое действие → keyPressed(). Непрерывное движение → keyIsDown() в draw(). Новички постоянно их путают, и от этого герой либо движется рывками по одному пикселю на нажатие, либо «залипает». Мы этого избежим.

Почему так вышло, что существуют два разных механизма? Дело в том, что у клавиатуры есть два разных вопроса, на которые хочется отвечать. Первый: «случилось ли только что нажатие?» — это момент, точка во времени, как вспышка. Второй: «держит ли пользователь клавишу сейчас?» — это длительность, состояние, которое тянется во времени. Дверной звонок отвечает на первый вопрос, педаль газа — на второй. p5.js даёт тебе по инструменту на каждый, потому что одним инструментом обе задачи красиво не решить. Если бы у тебя был только звонок, плавно вести героя пришлось бы дробным стуком по клавише; если бы только газ — невозможно было бы поймать ровно один момент нажатия для прыжка или паузы. Имея оба, ты выбираешь нужный под задачу — и всё ложится естественно.

Как узнать, какую клавишу нажали

Когда срабатывает keyPressed(), p5.js кладёт информацию о нажатой клавише в две встроенные переменные:

  • key — это символ клавиши в виде строки: например 'a', 'W', ' ' (пробел). Удобно для букв и цифр.
  • keyCode — это числовой код клавиши. Нужен для клавиш, у которых нет печатного символа: стрелки, Enter, Shift. У p5.js для них есть готовые имена-константы: LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, ENTER, SHIFT.

То есть для буквы r ты проверяешь key === 'r', а для стрелки влево — keyCode === LEFT_ARROW (или keyIsDown(LEFT_ARROW)). Разберём оба на живых примерах.

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

Пример 1: keyPressed() — цыплёнок прыгает по пробелу

Начнём с дверного звонка. Хотим: нажал пробел — CodeChick подпрыгнул на месте. Это разовое действие, значит используем keyPressed().

let y = 200;        // вертикальная позиция цыплёнка
let jump = false;   // прыгает ли он прямо сейчас

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

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

  // если прыгает — поднимем повыше, иначе стоит внизу
  let drawY = jump ? y - 60 : y;

  fill(255, 209, 64);
  circle(200, drawY, 80);     // тело
  fill(255, 140, 30);
  triangle(230, drawY - 5, 255, drawY, 230, drawY + 5);  // клюв
}

function keyPressed() {
  if (key === ' ') {   // нажали пробел
    jump = !jump;      // переключаем прыжок
  }
}

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

Главное здесь — keyPressed() вызывается p5.js сам, тебе не нужно её нигде вызывать вручную. Ты только описываешь, что должно случиться при нажатии. А проверка key === ' ' отсеивает все клавиши, кроме пробела: нажмёшь букву — ничего не произойдёт.

Пример 2: keyIsDown() — едем стрелками

Теперь педаль газа. Хотим настоящее управление: зажал стрелку — цыплёнок едет в эту сторону, отпустил — встал. Это непрерывное движение, значит keyIsDown() внутри draw().

let x = 200;
let y = 200;
let speed = 4;   // на сколько пикселей сдвигаемся за кадр

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

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

  // проверяем стрелки прямо сейчас, каждый кадр
  if (keyIsDown(LEFT_ARROW))  { x = x - speed; }
  if (keyIsDown(RIGHT_ARROW)) { x = x + speed; }
  if (keyIsDown(UP_ARROW))    { y = y - speed; }
  if (keyIsDown(DOWN_ARROW))  { y = y + speed; }

  fill(255, 209, 64);
  circle(x, y, 70);
  fill(255, 140, 30);
  triangle(x + 26, y - 5, x + 48, y, x + 26, y + 5);
}

Результат: жёлтый цыплёнок стоит в центре. Пока ты держишь стрелку вправо — он плавно едет вправо, по 4 пикселя за кадр; зажмёшь вверх — поднимается. Можно зажать две стрелки сразу (например вправо и вверх) — и он поедет по диагонали! Отпустил все клавиши — мгновенно замер. Это уже похоже на управление персонажем в настоящей игре.

Почему именно keyIsDown(), а не keyPressed()? Потому что эта функция спрашивает: «зажата ли клавиша в этот самый кадр?» И раз draw() крутится десятки раз в секунду, движение получается плавным и непрерывным, пока палец на клавише. А ещё — несколько if без else позволяют проверять все стрелки независимо, поэтому диагональ и работает: и LEFT, и UP могут быть зажаты одновременно.

Пример 3: буквы меняют цвет цыплёнка

Соединим оба подхода. Стрелками двигаем (как в примере 2), а буквами r, g, y разово перекрашиваем CodeChick — это уже работа для keyPressed() и переменной key.

let x = 200;
let y = 200;
let speed = 4;
let r = 255, g = 209, b = 64;  // текущий цвет тела (жёлтый)

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

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

  if (keyIsDown(LEFT_ARROW))  { x = x - speed; }
  if (keyIsDown(RIGHT_ARROW)) { x = x + speed; }
  if (keyIsDown(UP_ARROW))    { y = y - speed; }
  if (keyIsDown(DOWN_ARROW))  { y = y + speed; }

  fill(r, g, b);
  circle(x, y, 70);
  fill(255, 140, 30);
  triangle(x + 26, y - 5, x + 48, y, x + 26, y + 5);
}

function keyPressed() {
  if (key === 'r') { r = 230; g = 90;  b = 90;  }   // красный
  if (key === 'g') { r = 120; g = 200; b = 120; }   // зелёный
  if (key === 'y') { r = 255; g = 209; b = 64;  }   // обратно жёлтый
}

Результат: цыплёнок по-прежнему ездит стрелками, но теперь нажатие буквы r мгновенно перекрашивает его тело в красный, g — в зелёный, а y возвращает родной жёлтый. Клюв остаётся оранжевым. Движение (стрелки, непрерывно) и смена цвета (буквы, разово) живут вместе и не мешают друг другу.

Этот пример показывает золотое правило урока в действии: движение — через keyIsDown() в draw(), а разовые команды — через keyPressed(). Если бы мы попытались красить цыплёнка внутри draw() через keyIsDown('r'), оно бы тоже работало, но keyPressed() здесь чище: цвет меняется ровно в момент нажатия, а не «пока держишь».

Пример 4: keyCode внутри keyPressed() — шаг по стрелке

Иногда стрелка нужна не для плавного хода, а именно для разового шага — как в пошаговой игре или редакторе: нажал стрелку — фигура сдвинулась на одну клетку, и стой. Тут стрелка становится «дверным звонком», и ловить её надо в keyPressed(), но уже не через key (символа-то у стрелок нет!), а через keyCode.

let col = 4;   // номер клетки по горизонтали (0..7)

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

function draw() {
  background(135, 206, 235);
  let cell = 50;                 // размер клетки
  let cx = col * cell + cell / 2; // центр клетки по x

  fill(255, 209, 64);
  circle(cx, 200, 40);
  fill(255, 140, 30);
  triangle(cx + 15, 197, cx + 28, 200, cx + 15, 203);
}

function keyPressed() {
  if (keyCode === LEFT_ARROW)  { col = col - 1; }
  if (keyCode === RIGHT_ARROW) { col = col + 1; }
  col = constrain(col, 0, 7);   // не вылезаем за 8 клеток
}

Результат: цыплёнок стоит на сетке-клеточке. Каждое отдельное нажатие стрелки влево или вправо перемещает его ровно на одну клетку (50 пикселей) в эту сторону — как шашку по доске. Держать клавишу бесполезно: пока не отпустишь и не нажмёшь снова, он стоит. У краёв constrain() не даёт ему уйти за пределы восьми клеток.

Сравни Пример 2 и Пример 4: одни и те же стрелки, но поведение противоположное. Во втором — плавный непрерывный ход (keyIsDown в draw), в четвёртом — дискретные шаги по одному нажатию (keyCode в keyPressed). Это и есть то самое разделение на «газ» и «звонок», только теперь обе роли играет одна и та же клавиша-стрелка. Какую брать — решаешь ты, исходя из того, какую игру делаешь.

Шпаргалка: что брать под какую задачу

Чтобы не держать всё в голове, вот короткая таблица-памятка:

Что нужноИнструментЧто проверять
Плавно вести героя, пока держишьkeyIsDown() в draw()keyIsDown(RIGHT_ARROW)
Прыжок, выстрел, пауза — раз на нажатиеkeyPressed()key === ' '
Сменить цвет/режим по буквеkeyPressed()key === 'r'
Шаг по клетке за нажатие стрелкиkeyPressed()keyCode === LEFT_ARROW

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

  • Двигают героя внутри keyPressed() вместо keyIsDown(). Самая частая ловушка. Если написать движение в keyPressed(), цыплёнок сдвинется на один шаг за каждое нажатие — придётся долбить по клавише, как по кнопке лифта. Для плавного хода, пока клавиша зажата, нужен keyIsDown() в draw().

  • Сравнивают букву с заглавной, а жмут строчную (или наоборот). key === 'R' не сработает, если включён обычный регистр и Shift не зажат — там придёт 'r'. Если хочешь ловить букву независимо от регистра, проверяй обе: if (key === 'r' || key === 'R').

  • Стрелки проверяют как символ. У стрелок нет печатного символа, поэтому key === 'LEFT_ARROW' или key === '←' — это мимо. Стрелки ловят только по коду: keyCode === LEFT_ARROW или keyIsDown(LEFT_ARROW). Имена LEFT_ARROW и подобные — это готовые числа-константы p5.js, кавычки им не нужны.

  • Лишний else между if-проверками стрелок. Если написать if (...LEFT...) {} else if (...RIGHT...) {}, то одновременно зажать вправо и вверх не получится — диагональ сломается, сработает только первая клавиша. Для независимых направлений используй отдельные if без else.

  • Скетч не реагирует на клавиши вообще. Чаще всего холст просто «не в фокусе»: браузер шлёт клавиши тому окну, на которое ты последним кликнул. Кликни мышкой по самому холсту — и управление оживёт. На сайте перед игрой с клавишами всегда сначала ткни в картинку скетча.

  • Стрелки прокручивают страницу вместо героя. Браузер по умолчанию сам реагирует на стрелки и пробел — листает страницу. Если из-за управления героем прокручивается весь сайт, добавь в конец keyPressed() строку return false; — это говорит браузеру «я обработал клавишу сам, дальше не передавай». Маленькая, но очень спасительная деталь в играх.

Мини-практика: управляемый CodeChick

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

  1. Возьми за основу Пример 2 со стрелками и переменными x, y.
  2. Добавь границы: после всех проверок стрелок не дай цыплёнку уйти с холста. Используй constrain(): x = constrain(x, 35, 365); и так же для y. Теперь он будет упираться в стенки, а не улетать.
  3. Добавь ускорение по Shift: заведи переменную скорости и в draw() сделай let speed = keyIsDown(SHIFT) ? 9 : 4;. Зажал Shift вместе со стрелкой — цыплёнок несётся вдвое быстрее.
  4. Добавь разовое действие на пробел через keyPressed(): например, по пробелу телепортируй цыплёнка обратно в центр (x = 200; y = 200;) — кнопка «домой».

Когда базовый вариант заработает, попробуй усложнить. Пусть буква w увеличивает размер цыплёнка, а s уменьшает — это ещё одна пара разовых команд через keyPressed() и переменную диаметра. Так ты соединишь сразу четыре механики в одном скетче: непрерывное движение (стрелки), модификатор скорости (Shift), границы холста (constrain) и разовые команды (пробел, буквы). Это уже не учебный пример, а маленькая интерактивная игрушка, в которой осмысленно сочетаются оба способа читать клавиатуру.

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

Итоги

Сегодня ты дал CodeChick клавиатуру и научил его слушаться рук:

  • keyPressed() — дверной звонок: срабатывает один раз в момент нажатия. Для разовых действий — прыжок, смена цвета, телепорт.
  • keyIsDown(код) — педаль газа: отвечает true, пока клавиша зажата. Вызывай в draw() для плавного непрерывного движения.
  • key хранит символ клавиши (для букв: key === 'r'), а keyCode — числовой код (для стрелок: LEFT_ARROW, UP_ARROW и т. д.).
  • Отдельные if без else позволяют зажимать несколько клавиш сразу — отсюда движение по диагонали.

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

Проверьте себя
1. Какую функцию использовать, чтобы герой плавно двигался, пока ты держишь стрелку зажатой?
AkeyPressed()
BkeyIsDown() внутри draw()
CmousePressed()
Dsetup()
2. Сколько раз срабатывает keyPressed(), если ты нажал клавишу и держишь её, не отпуская?
AОдин раз — в момент нажатия
BКаждый кадр, пока держишь
CНи разу
DДважды: при нажатии и при отпускании
3. Как правильно проверить, что зажата стрелка влево?
Akey === 'LEFT_ARROW'
Bkey === '←'
CkeyIsDown(LEFT_ARROW)
DkeyPressed(LEFT)
4. Почему стрелки в Примере 2 написаны как отдельные if без else?
AТак короче писать
Belse вызвал бы ошибку
CЧтобы можно было зажать несколько стрелок сразу и двигаться по диагонали
Dp5.js не поддерживает else
5. В какой переменной p5.js хранит символ нажатой буквенной клавиши?
AkeyCode
Bkey
Cchar
Dbutton
6. Скетч вообще не реагирует на клавиши. Что проверить в первую очередь?
AКликнуть мышью по холсту, чтобы он получил фокус
BУвеличить frameRate
CЗаменить keyIsDown на mouseIsPressed
DУбрать background() из draw()