Один и тот же корабль на любом экране

Дневной рейтинг честен только тогда, когда пролететь один и тот же участок пещеры одинаково сложно на старом маленьком телефоне, планшете со 144 Гц и в браузере на компьютере. Вот что для этого потребовалось и что мне не удалось уравнять.

Дневник разработки · 20 сентября 2026 · все записи

Пост о ежедневной пещере был о том, как сделать мир одинаковым для каждого игрока. Этот пост о другой половине честности: как заставить корабль вести себя одинаково. Есть два отдельных способа, которыми устройство может изменить то, как играется игра, и им нужны два отдельных решения. Один - как часто обновляется экран. Другой - насколько большой экран.

Проблема первая: частота обновления

У корабля есть позиция и скорость. Каждый кадр гравитация или тяга меняют скорость, а скорость меняет позицию. Очевидный способ это записать таков:

vy += accel * dt;     // update velocity
py += vy * dt;        // then move by the NEW velocity

Выглядит безобидно, но здесь есть тонкая ошибка. Вторая строка делает вид, будто корабль уже провёл весь кадр, двигаясь со скоростью конца кадра. Если корабль ускоряется, это даёт перебег на половину accel × dt² каждый кадр. Загвоздка в том, что dt, длина кадра, разная на каждом устройстве, поэтому и перебег разный, и корабль летел по немного разной траектории при каждой частоте обновления.

Я измерил это, удерживая тягу в течение полсекунды. Точный ответ - 225 пикселей. Старый интегратор давал 228 при 144 Гц, 232 при 60 Гц и 240 при 30 Гц. Три устройства, три разных расстояния при одинаковом вводе.

Решение - правило трапеций, которое использует среднее значение скорости до и после кадра:

const vyPrev = vy;
vy += accel * dt;
py += (vyPrev + vy) * 0.5 * dt;   // average speed over the frame

Для постоянного ускорения это точно, независимо от длины кадра. Повторное воспроизведение одинаковой последовательности ввода теперь даёт разброс 0,000 пикселей в диапазоне от 12 до 144 Гц. Раньше было 4,1 пикселя. Тест проверяет обе половины: что новый интегратор совпадает на всех частотах обновления и что старый действительно расходился, поэтому если кто-то откатит это изменение, тест провалится, а не рейтинг тихо сместится.

Две честные оговорки

Это не устранило разрыв в счёте между частотами обновления. Я этого ожидал. Симулированный пилот всё равно набирал примерно на треть больше очков при 144 Гц, чем при 60 Гц, даже после исправления. Этот разрыв не связан с физикой. Это разрешение ввода: палец может переключиться с зажатия на отпускание только на границе кадра, поэтому более быстрый экран действительно даёт более точное управление. Никакой интегратор это не исправит, а бот, переключающий ввод каждый кадр, преувеличивает это по сравнению с человеком. Физическая половина теперь точна, а остальное - свойство железа.

Это очень небольшой нерф при 60 Гц. Полусекундный подъём теперь покрывает 225 пикселей вместо 232. Я оставил это без компенсации. Если тестирование на реальном устройстве когда-нибудь покажет, что корабль вялый, рычагом будет константа тяги - примерно на 3% больше восстановило бы старый полусекундный манёвр. Кстати, хороший момент сказать, что тягу уже дважды меняли по отзывам игроков, и эта история - причина, по которой я осторожен, трогая её.

Проблема вторая: высота экрана

Гравитация в пикселях в секунду в квадрате означает совершенно разные вещи на телефоне высотой 375 пунктов и на планшете высотой 520 пунктов. Если бы константы были одинаковыми, тот же подъём пересекал бы гораздо меньшую долю экрана на планшете, и игра ощущалась бы тяжелее.

Поэтому константы определены относительно эталонной высоты в 440 пунктов, высоты в альбомной ориентации самого большого iPhone, на котором ощущение игры настраивалось и тестировалось. На любом другом экране гравитация, тяга и предельная скорость умножаются на H / 440. Поскольку половина зазора коридора тоже масштабируется с высотой, каждая траектория геометрически подобна эталонной: та же доля коридора, пересекаемая за секунду, то же время, чтобы добраться от одной стены до другой, та же ощущаемая отзывчивость. Больший экран получает более крутое ускорение в пикселях, а меньший - более плавное, намеренно.

Это влечёт за собой правило для всего будущего кода: всё, что рассуждает о вертикальном движении, должно основываться на соотношениях. Доля предельной скорости, доля высоты. Никогда не сравнивать сырую константу в пикселях в секунду со скоростью корабля, потому что это число верно только на одном устройстве. Если литерала не избежать, его умножают на тот же масштаб.

Веб-версия следует тому же правилу. Браузерная сборка ограничивает свой холст размером 956 на 440, тем же размером, что и у эталонного телефона, поэтому игра в браузере является точной пиксельной и физической копией. У более ранней веб-сборки был собственный отдельный множитель ощущения, который существовал только для борьбы с вялостью, вызванной другим ограничением, и был удалён, как только ограничение совпало.

Проблема третья: ширина экрана

Высота - это вертикальная ось. Ширина - горизонтальная, и она более тонкая. Препятствия расставлены на фиксированных расстояниях в мире. Пещера прокручивается со скоростью, которая включает коэффициент ширины экрана. Поэтому на более широком экране те же препятствия проходят мимо быстрее в экранных терминах, а время на реакцию на каждое из них уменьшается пропорционально ширине.

Это была настоящая брешь. Большой Android-планшет шириной около 1280 пунктов сталкивался с общей пещерой примерно в 1,75 раза быстрее, чем маленький телефон. Поэтому ширина ограничена 956 пунктами на любой платформе - шириной самого широкого iPhone в альбомной ориентации. Устройство шире этого получает чёрные полосы слева и справа. Это ничего не меняет на iPhone и ограничивает только крупные Android-устройства и настольные браузеры.

У этого ограничения есть неисправленный остаток. Радиус столкновения игрока масштабируется с шириной, а коридор масштабируется с высотой, поэтому на более квадратном планшете корабль немного меньше относительно зазора, чем на телефоне, примерно на 20% снисходительнее. Исправить это значит изменить хитбокс, а это изменение ощущения, которое требует собственного плейтеста, и оно ошибается в сторону снисходительности, так что может подождать.

Тестировать честность, а не просто утверждать её

Ничего из этого не видно во время игры. Нельзя почувствовать несколько пикселей отклонения, и никто не играет одну и ту же пещеру на шести устройствах одновременно. Поэтому у каждого утверждения есть тест, который его воспроизводит: физический тест интегрирует фиксированный ввод при частотах обновления от 12 до 144 Гц, а тест пещеры воспроизводит реальные генераторы на шести размерах устройств и сравнивает результат побайтово. По моему опыту, непроверенное свойство честности уже незаметно сломалось.

Короче говоря, TUNL честен между экранами тремя отдельными способами (частота обновления, высота и ширина), у каждого своя причина, своё решение и свой остаток. Ты можешь увидеть результат сам, на любом устройстве, которое у тебя есть, в сегодняшней пещере.

Играть в пещеру дня