Теперь кот будет двигаться, пока ты удерживаешь стрелку, и останавливаться после отпускания. Мы продолжим прежний цикл: координаты меняются по времени, а обработчики клавиатуры только запоминают намерение игрока. Добавим границы и одинаковую скорость по прямой и диагонали.
Открыть учебную сцену отдельно
Добавляем управление с клавиатуры
start содержит те же файлы, что финал урока про игровой цикл. При старте кот пока сам идёт вправо. Рисунок, 480 × 320, рамка героя 32 × 40, кнопки и RAF сохраняются. Мы заменим только правило движения и добавим ввод. Открой проект по своему HTTP-адресу.
Для этой сцены нужна клавиатура. Кнопки запуска и сброса доступны мышью и через Tab/Enter, но экранного джойстика здесь нет. На телефоне можно проверить размер рисунка и кнопки; это не означает, что движение уже поддерживает сенсорное управление.
Включаем управление только в игровом поле
Стрелки нужны и игре, и чтению страницы. Если перехватывать их на всём document, игра будет мешать статье. Добавь к существующему холсту tabindex="0" и понятную подпись:
<canvas id="game" width="480" height="320" tabindex="0"
aria-label="Игровое поле: управление стрелками"
aria-describedby="instructions">Браузер не поддерживает Canvas.</canvas>
Абзацу с управлением над холстом дай id="instructions". Значение tabindex="0" включает холст в обычный порядок Tab. Стиль canvas:focus-visible в файлах подчёркивает активное поле. Рамка — подсказка, куда сейчас пойдёт ввод. Не убирай её ради оформления.
Замени handlePlay, чтобы кнопка сначала передавала фокус полю. canvas.focus() вызывает обычное действие браузера, а не запускает отдельный игровой цикл:
function handlePlay() {
canvas.focus();
startLoop();
}
Запоминаем нажатые клавиши
При удержании клавиши браузер может повторять keydown. Если каждый повтор сдвигает кота на десять пикселей, скорость зависит от настроек клавиатуры. Вместо этого запишем true при нажатии и false при отпускании. Сто одинаковых присваиваний не увеличивают скорость.
В game.js добавь объект и функции перед update. На первом этапе обрабатываем только левую и правую стрелки. event — объект события, который браузер передаёт обработчику; event.code обозначает физическую клавишу. setKey возвращает, была ли это нужная нам клавиша:
const keys = { left: false, right: false, up: false, down: false };
function clearKeys() {
keys.left = false;
keys.right = false;
keys.up = false;
keys.down = false;
}
function setKey(code, pressed) {
if (code === "ArrowLeft") keys.left = pressed;
else if (code === "ArrowRight") keys.right = pressed;
else return false;
return true;
}
function handleKeyDown(event) {
if (setKey(event.code, true)) event.preventDefault();
}
function handleKeyUp(event) {
if (setKey(event.code, false)) event.preventDefault();
}
preventDefault() запрещает стандартное действие этого события — для обработанной стрелки это прокрутка. Другие клавиши, в том числе Tab, не блокируются. Обработчики будут висеть на холсте, поэтому стрелка на кнопке, в тексте или на внешней статье не становится игровым вводом.
Останавливаем кота, когда поле теряет фокус
Игрок может удерживать стрелку и переключить окно до keyup. Старое значение true тогда осталось бы в игре. Добавь clearKeys() в stopLoop сразу после running = false. Это очистит ввод при паузе, сбросе и скрытии. Ещё зарегистрируй следующие события один раз, не внутри цикла или кнопки:
function handleInputBlur() {
clearKeys();
handlePause();
}canvas.addEventListener("keydown", handleKeyDown);
canvas.addEventListener("keyup", handleKeyUp);
canvas.addEventListener("blur", handleInputBlur);
window.addEventListener("blur", handleInputBlur);
blur означает потерю фокуса. Мы ставим паузу и обнуляем ввод, когда фокус уходит с поля или из окна браузера. Чтобы продолжить, нажми кнопку ещё раз. Это намеренное правило: открытая Console тоже может забрать фокус. Пауза в этом случае исправна.
Двигаем кота влево и вправо
Замени прежний автоматический update следующими двумя функциями. dx и dy — направление, а не уже пройденное расстояние. На первом этапе dy всегда ноль. Если обе противоположные стрелки зажаты, −1 и +1 взаимно уничтожаются:
function update(dt) {
let dx = 0;
let dy = 0;
if (keys.left) dx = dx - 1;
if (keys.right) dx = dx + 1;
player.x = player.x + dx * speed * dt;
player.y = player.y + dy * speed * dt;
keepInside();
}
function keepInside() {
player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
player.y = Math.max(0, Math.min(canvas.height - player.height, player.y));
}
keepInside сначала выбирает не больше допустимого максимума, затем не меньше нуля. Правая граница для левого края героя — 448, нижняя — 280. Использовать радиус здесь нельзя: у героя прямоугольная рамка, а координата — её верхний левый угол. Дробные координаты допустимы; округлять каждый кадр не нужно.
Запусти step-01. Удерживай вправо, отпусти, нажми обе стрелки. Дойди до границы. Нажми Tab: движение должно остановиться, а фокус — уйти с поля. После возвращения кнопкой «Продолжить» прежняя стрелка не остаётся зажатой. Если вся страница перестала прокручиваться стрелками, проверь, к какому элементу прикреплён keydown.
Добавляем движение вверх, вниз и по диагонали
В setKey перед последним else return false добавь распознавание двух оставшихся стрелок:
else if (code === "ArrowUp") keys.up = pressed;
else if (code === "ArrowDown") keys.down = pressed;
В update после проверок left/right добавь:
if (keys.up) dy = dy - 1;
if (keys.down) dy = dy + 1;
Теперь работает step-02. При одновременном движении вправо и вниз обе координаты меняются на полное расстояние. Пара (dx, dy) задаёт направление; такую пару называют вектором. Получается более длинная диагональ: вектор (1, 1) имеет длину примерно 1,414. Это промежуточная версия для наблюдения: по диагонали кот пока движется быстрее.
Делаем скорость по диагонали такой же
Math.hypot(dx, dy) вычисляет длину по двум сторонам: для (1, 0) это 1, для (1, 1) — примерно 1,414. Если разделить обе координаты направления на эту длину, направление сохранится, но общая длина станет 1. Такое приведение называют нормализацией. Проверка length > 0 защищает от деления на ноль, когда клавиш нет.
Между определением направления и изменением позиции вставь блок с Math.hypot. Полный update финальной версии выглядит так; остальные функции из предыдущего этапа сохраняются:
function update(dt) {
let dx = 0;
let dy = 0;
if (keys.left) dx = dx - 1;
if (keys.right) dx = dx + 1;
if (keys.up) dy = dy - 1;
if (keys.down) dy = dy + 1;
const length = Math.hypot(dx, dy);
if (length > 0) {
dx = dx / length;
dy = dy / length;
}
player.x = player.x + dx * speed * dt;
player.y = player.y + dy * speed * dt;
keepInside();
}
Проверь final: путь по диагонали за одинаковое время имеет такую же длину, как путь по горизонтали. Сумма изменений x + y для этой проверки не подходит — нужна длина Math.hypot(изменениеX, изменениеY). У стены холста пройденный путь может стать короче: часть намеренного движения ограничивается границей.
Самостоятельно: вторая схема клавиш
Добавь WASD вместе со стрелками, сохранив действие старой схемы. Физические коды — KeyA, KeyD, KeyW, KeyS. Они не зависят от напечатанной буквы при смене раскладки. Не меняй скорость, цикл или рисунок.
Проверь необычный случай: держи A и стрелку влево одновременно, затем отпусти только A. Герой должен продолжить идти влево до отпускания стрелки. Поэтому одного присваивания keys.left = false на любое отпускание недостаточно. Сохрани состояние каждой физической клавиши отдельно, затем вычисляй направление через «или». Объект со свойствами по коду и доступ heldCodes[code] уже известны из урока про объекты.
Подсказка 1: запомни каждую клавишу отдельно
Сначала запомни конкретную физическую клавишу. Затем left равно истине, если зажата хотя бы одна из двух левых клавиш. Не складывай их скорости: две клавиши одного направления не должны разгонять кота.
Подсказка 2: очистка
При паузе замени объект физических клавиш новым пустым объектом и сбрось четыре направления. Проверяй heldCodes.KeyA === true: отсутствующее свойство — это не удержание.
Разбор после самостоятельной попытки
В решении отдельный heldCodes хранит физические клавиши, а keys хранит четыре направления. Смешанное удержание одной стороны, противоположные стороны, другая раскладка и потеря фокуса должны сохранять ожидаемое поведение. Файлы находятся в solutions/wasd просмотрщика и ZIP. Проверить пример решения.
Проверка управления
- Удержание даёт непрерывное движение, отпускание останавливает; повтор keydown не разгоняет.
- Стрелки обрабатываются только на активном холсте; Tab позволяет выйти из него.
- Переключение окна очищает ввод и ставит паузу; после продолжения кот стоит, пока ты снова не нажмёшь стрелку.
- Кот целиком внутри 480 × 320; диагональ имеет ту же скорость.
- Вторая схема прошла проверку смешанного удержания и сброса.
Справочно: keydown и фокус, KeyboardEvent.code, потеря фокуса и preventDefault на MDN.