Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 7 из 25 ~16 мин +50 XP

Мышь и тач-ввод

Мышь в Ebitengine — это две функции: позиция курсора и состояние кнопок, а тач-ввод добавляет пальцы с собственными идентификаторами. Обе модели опрашиваются так же, как клавиатура из шестого урока, — каждый тик в Update, и в этом уроке мы соберём клик-в-точку, прицел и общий код для десктопа и телефона.

Содержание урока · 10 разделов

Клавиатура из прошлого урока отвечает на вопрос «куда движется игрок», но у целого жанра — стратегий, тап-игр, головоломок, point-and-click — вопрос другой: «куда игрок показывает». Указатель точен: клавиша двигает героя вектором, а курсор называет конкретную точку холста. Здесь же возникает первое настоящее изменение стиля мышления в курсе: координата указателя — это не направление, а место, и с ним приходит своя семья вопросов. В какой системе координат движок отдаёт позицию курсора, если окно растянуто, а холст — 320 на 240? Чем клик отличается от удержания, если оба выглядят как зажатая кнопка? И как один и тот же код работает на десктопе мышью и на телефоне пальцем? Все четыре ответа — в этом уроке, и первый из них приятный: половину работы за вас уже сделал движок.

ebiten мышь: CursorPosition и кнопки#

Позиция курсора читается парой функций: ebiten.CursorPosition() возвращает два int — x и y, а добавленная в свежих версиях ebiten.CursorPositionF() — те же координаты в float64, с дробной точностью. Главное, что нужно знать про эти числа: движок отдаёт их уже в логических координатах холста. Документация формулирует это как «logical position, учитывающая масштаб экрана», и за этими словами стоит забота: какой бы ни был масштаб окна, растяжение из Layout и чёрные поля — движок сам переводит позицию указателя из оконных пикселей в систему координат вашей игры. При Layout 320 на 240 и окне вдвое больше курсор в правом нижнем углу игровой области вернёт примерно (320, 240), а не оконные (960, 720). Ручной пересчёт «окно минус поле, делить на масштаб» писать не нужно — и не нужно, потому что он ломает то, что движок уже учёл.

Кнопки опрашивает ebiten.IsMouseButtonPressed(button): константы MouseButtonLeft, MouseButtonMiddle, MouseButtonRight закрывают классическую трёхкнопочную мышь — по внутренностям это именованные алиасы MouseButton0, MouseButton1 и MouseButton2. У игровых мышей рядом живут MouseButton3 и MouseButton4: боковые кнопки читаются тем же вызовом с другой константой, без единой строки платформенного кода. Для колеса есть ebiten.Wheel() — пара float64 с горизонтальным и вертикальным смещением колеса или тачпад-скролла за тик, нулевая, когда колесо стоит; зум в картах и прокрутка инвентаря — её работа. Все три вызова — опрос за тик в Update, ровно та же модель, что у клавиатуры: никаких событий, только честные вопросы каждый тик.

Клик-в-точку: герой едет к курсору

GO
type Game struct {
        x, y   float64 // герой
        tx, ty float64 // цель, куда кликнули
}

func (g *Game) Update() error {
        mx, my := ebiten.CursorPosition()
        // клик ставит цель: координаты уже логические
        if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
                g.tx, g.ty = float64(mx), float64(my)
        }
        // движение к цели упрощено: полный разбор — в пятом уроке
        if dx, dy := g.tx-g.x, g.ty-g.y; dx != 0 || dy != 0 {
                g.x += dx * 0.05
                g.y += dy * 0.05
        }
        return nil
}

IsMouseButtonJustPressed срабатывает в одном тике — клик ставит цель ровно один раз, а не льёт цели потоком всё время удержания. CursorPosition отдаёт логические координаты холста, поэтому float64(mx) — единственное преобразование, которое нужно. Само движение к цели поджато до простой тяги для краткости.

Layout возвращает 320×240, окно растянуто вдвое, курсор — в правом нижнем углу игровой области. Что вернёт CursorPosition?

Клик против удержания: inpututil для кнопок#

Пара «зажата против только что» у мыши повторяет клавиатурную один в один. ebiten.IsMouseButtonPressed отвечает на состояние — кнопка зажата прямо сейчас; inpututil.IsMouseButtonJustPressed истинна в тике нажатия; inpututil.IsMouseButtonJustReleased — в тике отпускания; а inpututil.MouseButtonPressDuration возвращает, сколько тиков кнопку держат, — удобно для натяжения лука или зарядки удара. Практическое разбиение действий такое: одиночный выстрел, клик по кнопке интерфейса, выбор предмета — только JustPressed; полёт за курсором, рисование кистью, стрельба очередью — IsMouseButtonPressed на удержании; перетаскивание собирается из всех трёх: JustPressed начал тягать, IsMouseButtonPressed держит, JustReleased бросил. Заметьте, что и здесь работает правило шестого урока: функции inpututil обязаны вызываться в Update, не в Draw — кадр может выйти без тика, и тогда событие из Draw потеряется.

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

Общий указатель: палец или мышь

GO
// pointer возвращает позицию указателя: палец на телефоне,
// мышь на десктопе. down — «нажато прямо сейчас»
func pointer() (x, y float64, down bool) {
        if ids := ebiten.AppendTouchIDs(nil); len(ids) > 0 {
                tx, ty := ebiten.TouchPosition(ids[0])
                return float64(tx), float64(ty), true
        }
        mx, my := ebiten.CursorPosition()
        return float64(mx), float64(my),
                ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
}

AppendTouchIDs(nil) возвращает срез идентификаторов активных касаний и всегда пуст на десктопах, поэтому тач-ветка там молча пропускается. TouchPosition отдаёт координаты первого пальца. Иначе работают мышь и кнопка. Один вызов pointer() — и прицел, и выбор предметов живут на всех платформах без ветвлений по GOOS.

Что сделает тач-ветка на десктопе?

Прочитайте код и выберите, что он выведет:

func (g *Game) Update() error {
        ids := ebiten.AppendTouchIDs(nil)
        if len(ids) > 0 {
                // ветка пальца
        }
        // дальше — ветка мыши
        return nil
}

Тач-ввод: пальцы со своими идентификаторами#

Тач устроен иначе, чем мышь: пальцев может быть несколько, и каждому нужен адрес. В Ebitengine адрес — это ebiten.TouchID, а полный словарь тач-ввода такой: ebiten.AppendTouchIDs дописывает в переданный срез идентификаторы всех активных касаний; ebiten.TouchPosition(id) возвращает позицию конкретного пальца парой int и (0, 0), если касание уже кончилось; inpututil.AppendJustPressedTouchIDs выделяет пальцы, коснувшиеся экрана именно в этом тике; inpututil.IsTouchJustReleased и inpututil.TouchPressDuration закрывают те же вопросы отпускания и длительности, что и для кнопок. Есть и тонкий прибор: inpututil.TouchPositionInPreviousTick возвращает позицию пальца в прошлом тике — разница текущей и прошлой позиций и есть скорость свайпа, а для пинч-зума берут расстояние между двумя пальцами и делят на него же из прошлого тика. Когда дробной точности не хватает — свайпы по сетке дрожат на усечении до пикселя, — в v2.10 появились TouchPositionF и TouchPositionFInPreviousTick: те же позиции в float64, без округления до целого. Официальный пример touch в репозитории движка собирает из этих примитивов жесты руками, и его пороги — полезные ориентиры: тап — касание, которое либо продержалось меньше тридцати тиков (при TPS 60 это около полусекунды), либо почти не съехало с точки касания; пан — одиночный палец, ушедший от точки касания; пинч — пара пальцев, чьё взаимное расстояние изменилось больше допустимой дельты.

Два практических замечания. Первое: мультитач — не экзотика, а две самые естественные жестовые механики, пинч и вращение, построены на втором пальце; держите идентификаторы в срезе и не предполагайте, что их всегда один. Второе — наследие: в пакете осталась функция TouchIDs, помеченная устаревшей с версии 2.2 — в новом коде используйте AppendTouchIDs, она переиспользует буфер и не создаёт мусор каждый тик. И не забудьте про телефон в целом: нативное мобильное приложение не имеет курсора, CursorPosition там всегда возвращает (0, 0), поэтому единственный живой ввод на телефоне — тач, и pointer() из этого урока — правильный каркас.

API мыши и тача урока#

ВызовПакетЧто даёт
ebiten.CursorPosition()ebiten(x, y) в логических координатах холста, масштаб учтён
ebiten.IsMouseButtonPressed(b)ebitenзажата ли кнопка: Left, Middle, Right
inpututil.IsMouseButtonJustPressed(b)inpututilклик ровно в этом тике
ebiten.Wheel()ebiten(xoff, yoff) колеса или тачпада; 0, если стоит
ebiten.AppendTouchIDs(ids)ebitenсрез TouchID активных пальцев; пуст на десктопах
inpututil.TouchPositionInPreviousTick(id)inpututilпозиция пальца в прошлом тике — скорость свайпа

Координаты курсора: масштаб уже учтён#

Вернёмся к координатам, потому что здесь спрятаны две тонкости, о которых лучше узнать из урока, а не из бага. Первая: до старта игрового цикла CursorPosition возвращает (0, 0) — спрашивать курсор в init бессмысленно, только из Update или позже. Вторая тонкость опаснее: если окно шире пропорций холста, движок при масштабировании оставляет поля — чёрные полосы слева и справа или сверху и снизу, и курсор над полем находится вне игровой области. При этом формула перевода в логические координаты поля не отбрасывает: над левой полосой x уедет в минус, над правой — за ширину холста. Движок честно отдаёт математически верное значение, а забота об отсечении — ваша: проверяйте, что координаты попадают в холст, прежде чем считать клик по миру.

Отсюда же мост к хит-тестам. Курсор — это точка, а игровой объект — прямоугольник: попадание точки в прямоугольник и станет основной операцией следующего урока про прямоугольники и позиции спрайта. Для окна и масштабов в целом — откуда берутся поля и как Layout дружит с окном — справочная база лежит в уроке про окно и холст; координаты курсора опираются на неё напрямую.

Курсор над полем масштабирования

Холст — 320×240, окно растянуто до 320×360: холст отмасштабирован в центре окна, сверху и снизу — поля по 60 пикселей. Что вернёт CursorPosition, когда игрок ведёт мышь над верхним полем?

Один код для трёх платформ#

Соберём платформенную картину целиком. Десктоп: мышь — главный указатель, тач-ветка всегда пуста, клавиатура из прошлого урока рядом. Телефон: CursorPosition всегда нулевая, и всё поведение живёт на AppendTouchIDs с пальцами; клавиатура существует только как подключённая периферия. Браузер: тот же WASM-бинарник умеет и мышь, и тач — на десктопе работает первая ветка pointer(), на телефоне вторая. Правило, из которого вытекает вся архитектура ввода курса: не выясняйте, чем управляет игрок, — опрашивайте все источники параллельно и позвольте пустым веткам молчать. Тогда перенос игры на новую платформу не трогает логику вовсе: меняются лишь физические жесты, а Update остаётся тем же.

Остался третий источник ввода, который мы сознательно откладываем: геймпад. Движок умеет его честно, и приборы знакомы по этому уроку: ebiten.AppendGamepadIDs перечисляет подключённые устройства с их GamepadID, ebiten.GamepadName называет устройство по-человечески, ebiten.IsGamepadButtonPressed и inpututil.IsGamepadButtonJustPressed работают по тому же принципу «зажата против только что», что клавиши и кнопки мыши. Курсу геймпад не нужен до итоговой аркады, а механика опроса у него ровно та же, что вы уже знаете. Каркас ввода полный: вектор из клавиш, точка из курсора, пальцы со своими идентификаторами. Дальше по курсу эти три источника станут входами одного автомата состояний игры, но фундамент опроса останется именно этим.

Правила указателя одним списком#

  • CursorPosition отдаёт логические координаты холста: масштаб окна движок учитывает сам
  • Ручной пересчёт координат курсора не нужен — и ломает прицел задваиванием масштаба
  • Клик — inpututil.IsMouseButtonJustPressed; удержание и перетаскивание — IsMouseButtonPressed плюс IsMouseButtonJustReleased
  • Тач адресуется идентификаторами: AppendTouchIDs даёт пальцы, TouchPosition — позицию каждого
  • AppendTouchIDs всегда пуст на десктопах — общий код мыши и тача безопасен
  • На нативных мобильных CursorPosition всегда (0, 0): единственный ввод телефона — тач

Соберите общий pointer()

Восстановите три пропуска: тач-список, позиция курсора и проверка кнопки.

func pointer() (float64, float64, bool) { if ids := ebiten.(nil); len(ids) > 0 { tx, ty := ebiten.TouchPosition(ids[0]) return float64(tx), float64(ty), true } mx, my := ebiten.() return float64(mx), float64(my), ebiten.(ebiten.MouseButtonLeft) }

Клик-в-точку от курсора до героя

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

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

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

Термин

CursorPosition

нажмите, чтобы перевернуть
Определение

ebiten.CursorPosition() — (x, y) в логических координатах холста: движок сам учёл масштаб окна и поля. На мобильных всегда (0, 0).

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Координаты курсора движок отдаёт уже в логической системе холста — масштаб и поля учтены, ручной пересчёт не нужен.
  • Кнопки — Left, Middle, Right — опрашиваются за тик; клик — inpututil.IsMouseButtonJustPressed, удержание — IsMouseButtonPressed.
  • Курсор над полем масштабирования даёт координаты за пределами холста: отсекайте их проверкой границ сами.
  • Тач адресуется идентификаторами: AppendTouchIDs плюс TouchPosition по каждому пальцу; свайпам помогает позиция прошлого тика.
  • AppendTouchIDs всегда пуст на десктопах, а CursorPosition на мобильных всегда (0, 0) — pointer() с двумя ветками закрывает все платформы.
  • Опрашивайте все источники ввода параллельно: пустая ветка молчит, и логика не зависит от платформы.

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

Соберите pointer() из урока и сделайте прицел: маленький спрайт или прямоугольник следует за точкой указателя каждый тик. Затем прикрутите стрельбу — счётчик выстрелов на IsMouseButtonJustPressed — и отдельно попробуйте очередь на IsMouseButtonPressed: наблюдение за разницей важнее результата. Добавьте клик-в-точку герою с нормализованным шагом из пятого урока и проверьте, что клик в угол холста приводит героя ровно к точке. Проверьте границы: клик над полом масштабирования не должен ставить цель — отсеките координаты за пределами холста. На телефоне или в эмуляторе проверьте тач-ветку: тап должен вести себя как клик.

Личные заметкиПусто
Хранятся только в вашем браузере (Ebitengine · Мышь и тач-ввод)

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

Почему курсор над чёрной полосой даёт координаты за пределами холста?

Масштабирование с полями рисует холст в центре окна, но формула перевода оконных координат в логические не отбрасывает области вне игровой части: над левой полосой x уходит в минус, над правой — за ширину. Движок отдаёт математически честное значение, а проверка попадания в холст — ваша обязанность, как и любая граница мира.

Как сделать пинч-зум двумя пальцами?

Возьмите AppendTouchIDs, выберите два первых идентификатора и посчитайте расстояние между их TouchPosition. В прошлом тике то же расстояние даёт TouchPositionInPreviousTick для обоих пальцев. Отношение расстояний — коэффициент зума текущего тика; умножайте на него масштаб камеры, и жест соберётся в десяток строк.

Нужен ли отдельный код ввода для WASM-версии?

Нет: API одинаков на всех платформах, и весь разбор уже сведён в pointer(). В браузере на десктопе работает мышь, на телефоне — тач, а тач-ветка на десктопе гарантированно пуста по документации. Отдельного кода требует только экзотика вроде блокировки контекстного меню правой кнопки — и это забота страницы-хоста, не игры.

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

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Chromedp25 уроков~11 чпрактика25 уроков уже доступны

Chromedp в Go: автоматизация Chrome без Selenium

Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.

  • Запускать Chrome из Go и управлять страницами
  • Находить элементы селекторами и ждать их появления
  • Кликать, заполнять формы и извлекать данные
  • Делать скриншоты, PDF и выполнять JavaScript