---
title: "Действия ввода вместо клавиш: KeyboardEvent.code, Gamepad API, мёртвая зона"
description: "Как отделить логику браузерной 2D-игры от конкретных клавиш: карта действий, KeyboardEvent.code вместо символа, стандартная раскладка Gamepad API, радиальная мёртвая зона стика с перемасштабированием и переназначение клавиш. Код проверен в Node.js 24."
slug: vvod-klaviatura-gejmpad-kasanie-v-2d-igre
date: 2026-11-01
updated: 2026-11-02
scheduled_at: "2026-11-01T09:00:00+03:00"
rubric: ustrojstvo-2d-igr
tags: ["vvod", "gejmpad", "klaviatura", "javascript", "brauzernye-igry"]
reading_time: 10
draft: false
summary: "Если игровая логика проверяет «нажат пробел», игра ломается на геймпаде, на телефоне и при переназначении клавиш. Правильнее ввести слой действий: логика спрашивает «нажат прыжок», а карта действий решает, какие клавиши, кнопки геймпада и зоны касания этому соответствуют. Для клавиатуры берут KeyboardEvent.code — физическое положение клавиши, не зависящее от раскладки; для геймпада — стандартную раскладку Gamepad API с опросом раз в кадр и радиальную мёртвую зону, которая отсекает дрожание стика."
---

## Коротко

Игровая логика должна спрашивать о действиях («прыжок», «влево»), а не о клавишах, — тогда клавиатура, геймпад и касание подключаются через одну карту действий. Для клавиатуры используйте `KeyboardEvent.code`, для геймпада — стандартную раскладку Gamepad API и опрос раз в кадр, для стика — радиальную мёртвую зону 0,15–0,2 с перемасштабированием. Код карты действий, мёртвой зоны и переназначения ниже проверен в Node.js 24 на поддельных событиях.

## Содержание

1. Почему проверка клавиш в логике ломает игру
2. Карта действий: один слой между устройством и игрой
3. KeyboardEvent.code против key: раскладка и подписи
4. Gamepad API: стандартная раскладка и опрос раз в кадр
5. Мёртвая зона стика: формула и перемасштабирование
6. Таблица: привязки по умолчанию
7. Код на JavaScript: действия, нажатие и отпускание, переназначение
8. Практический сценарий: экран настроек управления
9. Когда вам это не подойдёт

## Почему проверка клавиш в логике ломает игру

Проверка конкретной клавиши в логике привязывает игру к одному устройству и одной раскладке. Если условие прыжка — `e.key === " "`, то на геймпаде прыжка нет, на телефоне тоже, а левша, которому удобнее прыгать стрелкой «вверх», не может это изменить. Если условие — `e.key === "w"`, движение вперёд перестаёт работать при включённой русской раскладке, потому что та же клавиша печатает «ц».

Каждое такое место в логике приходится править при добавлении устройства. Чем позже вводится слой действий, тем больше этих мест.

## Карта действий: один слой между устройством и игрой

Карта действий — это таблица «действие → список входов», и логика игры читает только действия. Для каждого действия в каждом кадре вычисляются три значения: удерживается ли оно (`down`), нажато ли в этом кадре (`pressed`) и отпущено ли (`released`). Нажатие — это «удерживается сейчас и не удерживалось в прошлом кадре».

Для движения добавляют аналоговое значение `moveX` от −1 до 1: клавиши дают −1, 0 или 1, стик — промежуточные значения. Если клавиши нажаты, они важнее стика, иначе берётся стик.

## KeyboardEvent.code против key: раскладка и подписи

Для управления используют `KeyboardEvent.code`, потому что он описывает физическую клавишу. По MDN, `code` не меняется от раскладки и модификаторов и полезен как раз для игр, где клавиатура заменяет геймпад; при этом им нельзя определить, какой символ напечатан на клавише: код `KeyQ` на раскладке AZERTY соответствует клавише A. Отсюда правило:

- **логика и привязки** — по `code` (`KeyW`, `Space`, `ArrowUp`);
- **подписи в подсказках** — по раскладке пользователя: метод `navigator.keyboard.getLayoutMap()` вернёт символ для кода, но MDN помечает его как экспериментальный и доступный не во всех основных браузерах, поэтому нужна запасная подпись.

Поля ввода текста (имя игрока, чат) обрабатывают по `key`, иначе русский текст набрать нельзя.

## Gamepad API: стандартная раскладка и опрос раз в кадр

Геймпад в браузере не присылает событий нажатия кнопок — его состояние опрашивают через `navigator.getGamepads()` раз в кадр. Спецификация Gamepad рекомендует опрашивать как можно ближе к вызову `requestAnimationFrame` и с той же частотой. Ключевые факты из спецификации:

- кнопки отдают значение от 0 до 1 и признак `pressed`, оси — от −1 до 1;
- если браузер узнал устройство, `mapping` равен `"standard"`, и индексы фиксированы: кнопка 0 — нижняя в правой группе, 9 — правая в центральной группе, 12–15 — крестовина (верх, низ, лево, право), оси 0 и 1 — левый стик;
- до первого нажатия кнопки или движения стика `getGamepads()` возвращает пустой список — защита от снятия цифрового отпечатка;
- событие `gamepadconnected` для уже подключённого устройства может прийти только после нажатия кнопки.

Если `mapping` — пустая строка, раскладка неизвестна: предложите игроку назначить кнопки вручную.

## Мёртвая зона стика: формула и перемасштабирование

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

```text
длина = √(x² + y²)
если длина < зона:  выход = (0, 0)
иначе:  k = min(1, (длина − зона) / (1 − зона)) / длина;  выход = (x·k, y·k)
```

Перемасштабирование важно: без него на границе зоны значение скачет с 0 сразу до 0,2, и медленно красться не получится. С ним выход плавно идёт от 0 до 1.

## Таблица: привязки по умолчанию

Привязки ниже — стартовый набор для платформера; каждое действие получает минимум две клавиши и одну кнопку геймпада.

| Действие | Клавиатура (code) | Геймпад (standard) | Касание |
|---|---|---|---|
| Влево | `KeyA`, `ArrowLeft` | кнопка 14, ось 0 влево | левая половина экрана, зона джойстика |
| Вправо | `KeyD`, `ArrowRight` | кнопка 15, ось 0 вправо | то же |
| Прыжок | `Space`, `KeyW`, `ArrowUp` | кнопка 0 | круглая зона справа снизу, от 44×44 CSS-пикселей |
| Пауза | `Escape`, `KeyP` | кнопка 9 | значок в углу |

Размер зоны касания взят из критерия WCAG 2.5.5 (44×44 CSS-пикселя), подробнее — в статье про HUD и адаптивный интерфейс.

## Код на JavaScript: действия, нажатие и отпускание, переназначение

Код реализует карту действий, опрос раз в кадр, мёртвую зону и переназначение без двойных привязок. Он проверен в Node.js 24 на поддельных событиях клавиатуры и поддельном геймпаде; числа в комментариях совпадают с выводом.

```js
// Карта действий: логика игры спрашивает «нажат ли прыжок», а не «нажат ли пробел»
const bindings = {
  left:  { keys: ["KeyA", "ArrowLeft"],  pad: [14] },  // 14 — левая кнопка крестовины
  right: { keys: ["KeyD", "ArrowRight"], pad: [15] },
  jump:  { keys: ["Space", "KeyW", "ArrowUp"], pad: [0] }, // 0 — нижняя кнопка правой группы
  pause: { keys: ["Escape", "KeyP"], pad: [9] },
};

const keysDown = new Set();
function onKey(e) {
  // e.code — физическое положение клавиши: KeyW одинаков в раскладках QWERTY и ЙЦУКЕН
  if (e.type === "keydown") keysDown.add(e.code); else keysDown.delete(e.code);
}

// Радиальная мёртвая зона с перемасштабированием: вне зоны значение снова идёт от 0 до 1
function deadzone(x, y, dz) {
  const len = Math.hypot(x, y);
  if (len < dz) return { x: 0, y: 0 };
  const k = Math.min(1, (len - dz) / (1 - dz)) / len;
  return { x: x * k, y: y * k };
}

// Опрос раз в кадр: состояние «сейчас» и «в прошлом кадре» даёт нажатие и отпускание
let prev = {};
function pollActions(pad) {
  const now = {};
  for (const name in bindings) {
    const b = bindings[name];
    let on = b.keys.some(function (k) { return keysDown.has(k); });
    if (pad && pad.mapping === "standard") {
      on = on || b.pad.some(function (i) { return pad.buttons[i] && pad.buttons[i].pressed; });
    }
    now[name] = { down: on, pressed: on && !prev[name], released: !on && !!prev[name] };
  }
  const stick = pad ? deadzone(pad.axes[0], pad.axes[1], 0.2) : { x: 0, y: 0 };
  const keyX = (now.right.down ? 1 : 0) - (now.left.down ? 1 : 0);
  now.moveX = keyX !== 0 ? keyX : stick.x;
  prev = {};
  for (const name in bindings) prev[name] = now[name].down;
  return now;
}

// Переназначение: новая клавиша снимается с других действий, чтобы не было двойных привязок
function rebind(action, code) {
  for (const name in bindings) {
    bindings[name].keys = bindings[name].keys.filter(function (k) { return k !== code; });
  }
  bindings[action].keys.unshift(code);
}

// Проверка на поддельных событиях и геймпаде
const fakePad = function (buttons, axes) {
  return { mapping: "standard", axes, buttons: buttons.map(function (p) { return { pressed: p, value: p ? 1 : 0 }; }) };
};
const idle = fakePad(new Array(17).fill(false), [0, 0, 0, 0]);

onKey({ type: "keydown", code: "KeyW" });       // в русской раскладке e.key здесь «ц»
let a = pollActions(idle);
console.log(a.jump.pressed, a.jump.down);       // true true
a = pollActions(idle);
console.log(a.jump.pressed, a.jump.down);       // false true — удержание, не новое нажатие
onKey({ type: "keyup", code: "KeyW" });
console.log(pollActions(idle).jump.released);   // true

console.log(deadzone(0.15, 0.05, 0.2));         // { x: 0, y: 0 } — дрожание стика отсечено
const d = deadzone(0.6, 0, 0.2);
console.log(d.x.toFixed(2));                    // 0.50 — после зоны шкала снова от 0
console.log(deadzone(1, 0, 0.2).x);             // 1

const tilted = fakePad(new Array(17).fill(false), [0.6, 0.1, 0, 0]);
console.log(pollActions(tilted).moveX.toFixed(2)); // 0.50

rebind("jump", "KeyD");
console.log(bindings.jump.keys, bindings.right.keys); // [ 'KeyD', 'Space', 'KeyW', 'ArrowUp' ] [ 'ArrowRight' ]
```

Стик, отклонённый на 0,6 вправо, при зоне 0,2 даёт скорость 0,5: из диапазона 0,2–1 значение 0,6 находится ровно посередине.

## Практический сценарий: экран настроек управления

Экран переназначения строится поверх той же карты действий за 5 шагов:

1. **Список действий.** Выведите строки «Влево», «Вправо», «Прыжок», «Пауза» с текущими привязками; подписи клавиш берите из раскладки пользователя или из запасной таблицы.
2. **Режим ожидания.** По выбору строки ждите следующего `keydown` и вызывайте `rebind(действие, e.code)`; `Escape` отменяет ожидание.
3. **Конфликты.** Функция `rebind` снимает новую клавишу с других действий; покажите игроку, у какого действия она была.
4. **Мёртвая зона.** Добавьте ползунок от 0,05 до 0,4 с шагом 0,05 и сразу показывайте точку положения стика после фильтрации.
5. **Сохранение.** Сохраняйте карту в `localStorage` как JSON и проверяйте при загрузке, что каждое действие получило хотя бы один вход.

## Когда вам это не подойдёт

Слой действий окупается почти всегда, но есть случаи, где он избыточен или недостаточен:

- **Игра на одну кнопку:** если весь ввод — «нажать в любом месте», карта действий из одной строки ничего не упрощает.
- **Текстовый ввод и игры на набор слов:** там важен символ, то есть `key`, а не физическая клавиша.
- **Специальные контроллеры:** рули, педали и нестандартные устройства часто приходят с пустым `mapping`, и для них нужна ручная калибровка осей, которой здесь нет.

---

Мы разрабатываем **J2DRU** — браузерный 2D игровой движок; управление с клавиатуры, мыши, геймпада и касанием с переназначением клавиш входит в проверенные внутри разработки основы. Движок публично пока не открыт, заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=vvod-klaviatura-gejmpad-kasanie-v-2d-igre) (подача заявки не гарантирует приглашение).

**Источники (проверено: ноябрь 2026):** [MDN — KeyboardEvent: code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code), [MDN — Keyboard: getLayoutMap()](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/getLayoutMap), [W3C — Gamepad](https://w3c.github.io/gamepad/), [W3C — Understanding SC 2.5.5 Target Size (Enhanced)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html).

**Читайте также:**
- [Идеальный прыжок и секрет Coyote Time](/blog/idealnyj-pryzhok-i-sekret-coyote-time)
- [Как сделать Змейку на JS: таймер шага, хвост, переход через край](/blog/kak-sobrat-igru-zmejka-na-sobytiyah)
- [Игровой цикл и физика столкновений AABB](/blog/igrovoj-cikl-i-fizika-stolknovenij-aabb)
