Содержание урока · 10 разделов
До сих пор герой курса ехал вправо сам, как тележка без водителя: вектор направления был зашит в код, и пятый урок этим честно оговаривался. Игра начинается там, где игрок берёт управление в руки, а самый прямой способ дать ему руль — клавиатура. Здесь новичка ждёт развилка, которая делит все туториалы пополам: события против опроса. Событийная модель — «вот обработчик 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 и стрелок
GOtype 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 из этого урока?
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 дописывает в переданный срез все зажатые клавиши разом — официальный пример клавиатуры движка именно так их и собирает, сортирует и печатает.
Одноразовое действие: импульс на нажатие
GOimport "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 для удерживаемой клавиши?
Что покажут счётчики после секунды удержания?
Прочитайте код и выберите, что он выведет:
// игрок нажал и держит Space; TPS = 60, прошла секунда
func (g *Game) Update() error {
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
g.shots++
}
if ebiten.IsKeyPressed(ebiten.KeySpace) {
g.held++
}
return nil
}Имена клавиш и текстовый ввод#
Для интерфейса нужно уметь называть клавиши словами, и здесь работает 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-функций, число тиков и проверку «только что нажата».
Тик управления от опроса до шага
Соберите Update героя: опрос, защита от нулевого вектора, нормализация и шаг.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Клавиатурный слой курса собран: один опрос за тик, две пары функций для «зажато» и «только что», физические клавиши вместо букв и имена для интерфейса. Дальше управление становится всё более прямым: урок про мышь и тач-ввод добавляет позицию курсора и пальцы — там же выяснится, что движок уже пересчитал координаты курсора в логические, и это сюрприз для тех, кто успел написать пересчёт руками. Урок про коллизии AABB поставит движущегося героя в мир со стенами, а нормализацию из урока про движение стоит держать под рукой: readDirection этого урока — её законное место рядом. Те же два примитива — состояние и импульс — ждут вас у мыши и пальцев: седьмой урок переиспользует их один в один. Проверьте себя простым тестом: если в вашем Update прыжок вызывается через IsKeyPressed, вы прочитали урок зря — вернитесь и поставьте inpututil.IsKeyJustPressed.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Клавиатура в 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 документированы для десктопов и браузеров одинаково. Нюанс другой: у веб-игры клавиатуру у страницы нужно не потерять из виду, когда игрок кликнул мимо канваса, — тест это покажет сразу. На нативных мобильных приложениях клавиатурный ввод требует фокусируемого представления, а пальцем игрок управляется через тач из следующего урока.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chromedp в Go: автоматизация Chrome без Selenium
Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.
- Запускать Chrome из Go и управлять страницами
- Находить элементы селекторами и ждать их появления
- Кликать, заполнять формы и извлекать данные
- Делать скриншоты, PDF и выполнять JavaScript