---
title: "Ввод: клавиатура"
description: "Клавиатура в Ebitengine: IsKeyPressed и опрос за тик, WASD и стрелки в одной функции, IsKeyJustPressed для прыжков, KeyName и AppendInputChars — с кодом."
course: "Ebitengine: 2D-игры на Go"
url: "https://golangmaster.ru/courses/ebitengine/lessons/06-klaviatura/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Ввод: клавиатура

*Курс «Ebitengine: 2D-игры на Go», урок 6 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/ebitengine/lessons/06-klaviatura/*

Клавиатура — первый настоящий контроллер игрока, и в Ebitengine ebiten input сводится к опросу состояния каждый тик, без событий и обработчиков. Подключим к герою из пятого урока WASD и стрелки, разведём «зажата» и «нажата только что» и соберём одноразовые действия, которые не срабатывают шестьдесят раз в секунду.

До сих пор герой курса ехал вправо сам, как тележка без водителя: вектор направления был зашит в код, и пятый урок этим честно оговаривался. Игра начинается там, где игрок берёт управление в руки, а самый прямой способ дать ему руль — клавиатура. Здесь новичка ждёт развилка, которая делит все туториалы пополам: события против опроса. Событийная модель — «вот обработчик keydown, вызовите меня, когда игрок нажмёт» — привычна по вебу, но Ebitengine построена на другой идее: игровой цикл и так тикает 60 раз в секунду, поэтому проще и надёжнее спросить состояние клавиш прямо в Update. Никаких подписок, никаких коллбеков, никакого состояния на стороне библиотеки — чистая функция, которая возвращает правду о клавиатуре в момент вызова. В этом уроке мы собираем вектор направления из WASD и стрелок, добавляем одноразовые действия через пакет inpututil и разбираем имена клавиш для интерфейса.

### ebiten клавиатура: IsKeyPressed и опрос за тик

Главный клавиатурный вызов движка — ebiten.IsKeyPressed(key), где key — константа типа ebiten.Key: KeyA, KeyW, KeySpace, KeyLeft, KeyEscape и десятки других, по списку на каждую физическую клавишу. Функция возвращает bool — «зажата ли эта клавиши прямо сейчас» — и её принято вызывать каждый тик внутри Update: состояние меняется, а опрос читает свежее значение бесплатно. Это модель опроса, и она неожиданно удобна именно в играх: логика и так выполняется 60 раз в секунду, значит чтение ввода в том же месте исключает целый класс ошибок рассинхрона между событиями и состоянием мира.

Важная деталь, которую документация подчёркивает отдельно: ebiten.Key описывает физическую клавишу американской раскладки. Константа KeyQ — это клавиша, на которой Q стоит на QWERTY, а на клавиатуре Дворака — апостроф. Для игры это подарок: игрок с русской раскладкой жмёт ту же физическую кнопку, что и американец, и WASD работает одинаково без единой проверки раскладки. Символы с учётом раскладки — отдельная история, к ней вернёмся ниже. Пока запомните правило урока: движение читается в Update, один опрос за тик, и никогда — в Draw, частота которого зависит от монитора.

### Направление из WASD и стрелок

```go
type Game struct {
        x, y  float64 // позиция героя из пятого урока
        speed float64 // пикселей в секунду
}

// readDirection собирает вектор из двух схем: WASD и стрелки
func readDirection() (dx, dy float64) {
        if ebiten.IsKeyPressed(ebiten.KeyA) || ebiten.IsKeyPressed(ebiten.KeyLeft) {
                dx -= 1
        }
        if ebiten.IsKeyPressed(ebiten.KeyD) || ebiten.IsKeyPressed(ebiten.KeyRight) {
                dx += 1
        }
        if ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeyUp) {
                dy -= 1
        }
        if ebiten.IsKeyPressed(ebiten.KeyS) || ebiten.IsKeyPressed(ebiten.KeyDown) {
                dy += 1
        }
        return dx, dy
}

func (g *Game) Update() error {
        dx, dy := readDirection()
        if dx != 0 || dy != 0 {
                g.x += dx * g.speed / float64(ebiten.TPS())
                g.y += dy * g.speed / float64(ebiten.TPS())
        }
        return nil
}
```

readDirection объединяет обе схемы: каждое направление — логическое ИЛИ из WASD и стрелок, поэтому игрок сам выбирает привычный слой управления. Диагональ без нормализации даёт корень из двух — приём из пятого урока встанет сюда одной строкой. Шаг за тик — скорость, делённая на TPS: движение остаётся одинаковым на любом железе, как и договаривались.

**Вопрос:** Игрок зажал W и S одновременно. Что вернёт readDirection из этого урока?

- Ошибку: две противоположные клавиши — недопустимый ввод
- dx = 0, dy = -1: первая нажатая клавиша побеждает
- ✅ dx = 0, dy = 0: прибавления взаимно уничтожаются, герой стоит
- Случайное направление: порядок опроса не определён

Опрос работает сложением: W уменьшает dy на единицу, S тут же прибавляет единицу, и вектор обнуляется. Это самый честный исход — игрок жмёт противоположности, герой стоит на месте. Тот же вопрос по горизонтали: A с D дают dx = 0.

### ebiten input: зажата против только что нажата

IsKeyPressed отвечает на вопрос «зажата ли клавиша», но у ввода есть второй вопрос — «нажата ли она только сейчас, в этом тике». Для него в пакете github.com/hajimehoshi/ebiten/v2/inpututil живут inpututil.IsKeyJustPressed и симметричная IsKeyJustReleased: первая истинна ровно в одном тике — том, где клавиша перешла из состояния «отпущена» в «нажата», — вторая зеркально отмечает отпускание. Разница не академическая: прыжок, выстрел, пауза и подтверждение в меню — дискретные действия, им нужен один импульс на нажатие, а бег и движение — непрерывные состояния, им нужно держаться, пока палец на клавише. IsKeyJustReleased достраивает пару: она истинна в тике отпускания, и на ней строятся действия с накоплением — натяжение лука держится на IsKeyPressed, а выстрел случается в тик, когда игрок отпустил KeySpace, и сила заряда к этому моменту уже посчитана KeyPressDuration в тиках. Смешение этих примитивов — источник самых шумных багов первых игр.

Документация inpututil добавляет два строгих правила. Первое: функции пакета обязаны вызываться в Update, не в Draw — причина в том, что Update может выполниться несколько раз за кадр, а кадр может выйти вообще без Update, и собранные там события размажутся или потеряются. Второе: IsKeyJustPressed всегда возвращает false для сборных констант вроде KeyShift и KeyControl, под которыми прячутся две физические клавиши — для них берите конкретные KeyShiftLeft или KeyShiftRight. Тем же пакетом удобно пользоваться списком: inpututil.AppendPressedKeys дописывает в переданный срез все зажатые клавиши разом — официальный пример клавиатуры движка именно так их и собирает, сортирует и печатает.

### Одноразовое действие: импульс на нажатие

```go
import "github.com/hajimehoshi/ebiten/v2/inpututil"

type Game struct {
        shots   int
        running bool
}

func (g *Game) Update() error {
        // дискретное действие: ровно один раз на нажатие
        if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
                g.shots++
        }
        // непрерывное состояние: бег, пока Shift зажата
        g.running = ebiten.IsKeyPressed(ebiten.KeyShiftLeft)
        return nil
}
```

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

**Вопрос:** Сколько тиков подряд inpututil.IsKeyJustPressed вернёт true для удерживаемой клавиши?

- Все, пока клавиша зажата
- ✅ Ровно один — тик, в котором она была нажата
- Шестьдесят — первую секунду удержания
- Ни одного: функция работает только в Draw

JustPressed отмечает границу состояния: из «отпущена» в «нажата» клавиша переходит один раз, и ровно в этом тике функция истинна. Всё дальнейшее удержание — территория IsKeyPressed. А вызывать inpututil в Draw запрещает сама документация пакета.

### Что покажут счётчики после секунды удержания?

```go
// игрок нажал и держит Space; TPS = 60, прошла секунда
func (g *Game) Update() error {
        if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
                g.shots++
        }
        if ebiten.IsKeyPressed(ebiten.KeySpace) {
                g.held++
        }
        return nil
}
```

**Что выведет программа?**

- ✅ shots = 1, held = 60
- shots = 60, held = 60
- shots = 1, held = 1
- shots = 0, held = 60

IsKeyJustPressed сработала в первом тике удержания — shots вырос на единицу и больше не растёт. IsKeyPressed истинна каждый из шестидесяти тиков, поэтому held дошёл до 60. Именно поэтому выстрел пишут через inpututil, а движение — через основной опрос.

### Имена клавиш и текстовый ввод

Для интерфейса нужно уметь называть клавиши словами, и здесь работает ebiten.KeyName(key): она возвращает имя клавиши в текущей раскладке игрока — KeyQ на QWERTY-клавиатуре даст «q», а на AZERTY даст «a», потому что физическое место одно, а буквы на них разные. Функция документирована для десктопов и браузера и возвращает пустую строку в трёх случаях: у клавиши нет физического имени, платформа не поддерживает имена или игровой цикл ещё не стартовал. Подсказка «нажмите E, чтобы подобрать» — её законное место, и хорошая привычка — выводить KeyName, а не зашивать букву в строку: на AZERTY подсказка сама станет честной.

Печатный ввод — имя героя, чат, чит-коды — это другая функция: ebiten.AppendInputChars дописывает в переданный срез рун те символы, которые игрок напечатал за тик, с учётом раскладки, регистра и модификаторов. Документация делит обязанности жёстко: AppendInputChars — локализованные символы, Key — физические клавиши, а Control и прочие модификаторы обрабатываются через IsKeyPressed. Из этой пары собирается любой текстовый режим: руны из AppendInputChars складываются в строку, Escape через IsKeyJustPressed её закрывает, Enter подтверждает. Отдельно приятная мелочь для мобильных сборок: клавиатура на Android и iOS требует фокусируемого вида приложения — документация оговаривает это прямо, и лучше знать об этом до релиза, а не после жалобы игрока.

### Клавиатурный API: что когда брать

| Вызов | Пакет | Возвращает | Типовое применение |
| --- | --- | --- | --- |
| ebiten.IsKeyPressed(KeyA) | ebiten | bool — пока зажата | движение WASD и стрелками |
| inpututil.IsKeyJustPressed(key) | inpututil | bool — один тик | прыжок, выстрел, пауза |
| inpututil.IsKeyJustReleased(key) | inpututil | bool — один тик | отпустить натяжение лука |
| inpututil.AppendPressedKeys(keys) | inpututil | срез зажатых клавиш | отладка и запись ввода |
| ebiten.KeyName(key) | ebiten | имя в текущей раскладке | подсказки интерфейса |
| ebiten.AppendInputChars(runes) | ebiten | срез печатных рун | имя героя, чат |

### WASD и стрелки: две схемы, один код

Зачем поддерживать обе схемы сразу? Потому что они живут у разных игроков в разных руках: стрелки — правая рука на отдельном блоке, WASD — левая рука возле мыши, и эти два стиля сосуществуют в одной игре десятилетиями. Физическая модель ebiten.Key делает поддержку бесплатной: KeyA — это позиция клавиши, а не буква, поэтому русская, французская или немецкая раскладка не ломают ни одну из схем — раскладка влияет на символы, а не на клавиши. readDirection из этого урока уже читает оба слоя через ИЛИ, и единственное, что стоит вынести из этого кода дальше, — сами клавиши можно сделать данными, а не частью кода: карта «клавиша — направление» в поле структуры превращает поддержку третьей схемы управления в пару строк конфигурации, а биндинги «на горячую» — в загрузку из файла.

Второй сценарий двух слоёв — меню и игра. В интерфейсе перемещение по пунктам удобнее стрелками и подтверждается Enter, в бою работает WASD и Space; оба опрашиваются из одного Update параллельно, а экран решает, какой ввод сейчас главный. Когда пунктов меню станет много и появится история экранов, этот разбор продолжится в уроке про сцены и конечный автомат, а сейчас важно другое: опрос клавиатуры дёшев, читайте все нужные схемы каждый тик и не экономьте на вызовах — их стоимость нулевая рядом с любым DrawImage.

> **Живое место: прыжок, который дребезжит шестьдесят раз в секунду.** Классика первых дней: прыжок написан на IsKeyPressed, и пока Space зажата, каждый тик ставит герою импульс — тот взлетает столбом, а выстрел на удержании съедает обойму за секунду. Что сломалось бы дальше: меню на том же IsKeyPressed пролеталось бы насквозь, до последнего пункта. Лечится разбором примитивов: одноразовое действие — inpututil.IsKeyJustPressed, непрерывное состояние — IsKeyPressed.

### Границы клавиатурного ввода

Три честных ограничения, о которых стоит знать заранее. Первое — мобильные платформы: физической клавиатуры у телефона нет, IsKeyPressed осмыслен только при подключённой или аппаратной клавиатуре, а пальцем по стеклу игрок будет делать то же самое через тач — это целиком тема седьмого урока, и общий каркас Update от этого не изменится. Второе — тестирование: readDirection вызывает движок напрямую, и юнит-тест её не «нажмёт»; поэтому чистую часть — сбор вектора из четырёх флагов и нормализацию — выносите в функцию без движка, а тонкий слой опроса держите в одну строку вызова, как в коде этого урока. Третье — дисциплина места: ввод опрашивается в Update и складывается в поля структуры, Draw эти поля только читает; как только ввод начинает читаться в отрисовке, скорость игры начинает зависеть от монитора — та же болезнь, что и с движением в Draw из пятого урока. Отдельно у пакета есть списочные варианты AppendJustPressedKeys и AppendJustReleasedKeys: они вернут разом все клавиши, нажатые или отпущенные в этом тике, — удобно для записи демо и отладки ввода. На этом границы известны, и клавиатура складывается в аккуратный слой из трёх функций и одного правила.

> **Клавиши оживут только локально.** ebiten.IsKeyPressed в песочнице сайта не нажать — ей нужен локальный Go, окно операционной системы и фокус ввода. Зато логика урока — сбор вектора из флагов, разбор «зажата против только что», счётчики тиков — чистая математика: тематическая программа в песочнице прокручивает тики и печатает, как меняется вектор направления, прямо на странице.

### Правила ввода одним списком

- Опрос, а не события: ввод читается каждый тик в Update и никогда в Draw
- IsKeyPressed — «зажата», inpututil.IsKeyJustPressed — «нажата только что»
- Дискретные действия — прыжок, выстрел, пауза — только через JustPressed
- KeyA — физическая клавиша: WASD работает в любой раскладке без правок
- Для сборных клавиш вроде KeyShift у JustPressed всегда false — берите KeyShiftLeft
- Подсказки интерфейса — ebiten.KeyName, печатный ввод — ebiten.AppendInputChars

### Соберите тик управления

Восстановите три пропуска: пакет event-функций, число тиков и проверку «только что нажата».

```go
import "github.com/hajimehoshi/ebiten/v2/___"

func (g *Game) Update() error {
        dx, dy := readDirection()
        if dx != 0 || dy != 0 {
                g.x += dx * g.speed / float64(ebiten.___())
                g.y += dy * g.speed / float64(ebiten.TPS())
        }
        if g.onGround && inpututil.___(ebiten.KeySpace) {
                g.jump()
        }
        return nil
}
```

Подсказка: Функции «только что нажата» живут в пакете inpututil, частота тиков — ebiten.TPS, а одноразовый прыжок проверяет IsKeyJustPressed.

Правильные ответы в пропусках: `inpututil`, `TPS`, `IsKeyJustPressed`

### Тик управления от опроса до шага

Соберите Update героя: опрос, защита от нулевого вектора, нормализация и шаг.

Правильный порядок строк:

1. `func (g *Game) Update() error {`
2. `dx, dy := readDirection()`
3. `if dx == 0 && dy == 0 { return nil }`
4. `length := math.Hypot(dx, dy)`
5. `dx, dy = dx/length, dy/length`
6. `g.x += dx * g.speed / float64(ebiten.TPS())`
7. `g.y += dy * g.speed / float64(ebiten.TPS())`
8. `}`

Клавиатурный слой курса собран: один опрос за тик, две пары функций для «зажато» и «только что», физические клавиши вместо букв и имена для интерфейса. Дальше управление становится всё более прямым: урок про [мышь и тач-ввод](/courses/ebitengine/lessons/07-mysh-i-tach/) добавляет позицию курсора и пальцы — там же выяснится, что движок уже пересчитал координаты курсора в логические, и это сюрприз для тех, кто успел написать пересчёт руками. Урок про [коллизии AABB](/courses/ebitengine/lessons/09-kollizii-aabb/) поставит движущегося героя в мир со стенами, а нормализацию из [урока про движение](/courses/ebitengine/lessons/05-geometriya-dvizheniya/) стоит держать под рукой: readDirection этого урока — её законное место рядом. Те же два примитива — состояние и импульс — ждут вас у мыши и пальцев: седьмой урок переиспользует их один в один. Проверьте себя простым тестом: если в вашем Update прыжок вызывается через IsKeyPressed, вы прочитали урок зря — вернитесь и поставьте inpututil.IsKeyJustPressed.

### Термины урока

- **IsKeyPressed** — ebiten.IsKeyPressed(key) — «зажата ли клавиша сейчас». Опрос каждый тик в Update; для непрерывных состояний вроде движения и бега.
- **IsKeyJustPressed** — inpututil.IsKeyJustPressed(key) — истинно ровно в тике нажатия. Для одноразовых действий: прыжок, выстрел, пауза. Вызывать только в Update.
- **ebiten.Key** — Тип-константа физической клавиши американской раскладки: KeyA, KeySpace, KeyLeft. Раскладка ОС не влияет — важно физическое место клавиши.
- **KeyName** — ebiten.KeyName(key) — имя клавиши в текущей раскладке игрока; для десктопов и браузера. Пустая строка, если имени нет или цикл не стартовал.
- **AppendInputChars** — ebiten.AppendInputChars(runes) — печатные символы за тик с учётом раскладки и регистра. Для имени героя и чата; модификаторы — через IsKeyPressed.
- **AppendPressedKeys** — inpututil.AppendPressedKeys(keys) — дописывает в срез все зажатые клавиши разом. Рабочий инструмент отладки и записи ввода.

## Коротко: что запомнить

- Клавиатура в Ebitengine опрашивается, а не ловится событиями: один ebiten.IsKeyPressed за тик внутри Update.
- «Зажата» — IsKeyPressed, «нажата только что» — inpututil.IsKeyJustPressed из пакета inpututil, истинная ровно один тик.
- Одноразовые действия — прыжок, выстрел, пауза — живут на JustPressed, иначе срабатывают шестьдесят раз в секунду.
- ebiten.Key — физическая клавиша US-раскладки: WASD и стрелки не зависят от раскладки ОС.
- Для сборных клавиш вроде KeyShift JustPressed всегда false — нужны KeyShiftLeft и KeyShiftRight.
- Имена клавиш для интерфейса — ebiten.KeyName, печатный ввод с раскладкой — ebiten.AppendInputChars.

## Домашнее задание

Подключите readDirection к герою из пятого урока и добавьте нормализацию — убедитесь, что диагональ не быстрее прямой. Затем добавьте паузу: флаг paused переключается через inpututil.IsKeyJustPressed на KeyEscape, и в паузе движение замирает, но опрос ввода продолжается. Выведите отладочной строкой ebitenutil.DebugPrint список активных клавиш из inpututil.AppendPressedKeys и понаблюдайте, как он меняется при зажатии двух и трёх клавиш. Найдите в своём коде место, где одноразовое действие висит на IsKeyPressed, замените на IsKeyJustPressed и сравните поведение до и после. Напоследок выведите KeyName для KeyA и KeySpace и смените раскладку ОС: имя в подсказке должно обновиться.

## Вопросы и ответы по уроку

### Почему IsKeyPressed не ломается на русской раскладке?

Потому что ebiten.Key описывает физическую клавишу американской раскладки, а не символ: KeyW — это место, где W стоит на QWERTY, независимо от наклеек. Печатали вы «ы» или «w» — для движка это одна клавиша. Символы с учётом раскладки даёт отдельная функция AppendInputChars, и именно поэтому движение и печать не путаются.

### Можно ли обойтись без inpututil и самому отловить «только что нажата»?

Да: храните прошлое состояние клавиши в поле структуры и сравнивайте — было false, стало true, значит нажато только что. inpututil делает ровно это, плюс держит длительность удержания KeyPressDuration в тиках и список событий разом. Самописный вариант — полезное упражнение, библиотечный — короче и без состояния на каждые клавиши.

### Работает ли клавиатура в браузерной WASM-сборке?

Да: IsKeyPressed, KeyName и AppendInputChars документированы для десктопов и браузеров одинаково. Нюанс другой: у веб-игры клавиатуру у страницы нужно не потерять из виду, когда игрок кликнул мимо канваса, — тест это покажет сразу. На нативных мобильных приложениях клавиатурный ввод требует фокусируемого представления, а пальцем игрок управляется через тач из следующего урока.
