
Есть довольно интересная категория багов в играх, которые формально багами являются, но игрокам они нравятся, и они, наоборот, называют багом “нормальное поведение” и строчат про него репорты про криворуких погромистов. Там, где программист хорошо и физически корректно сделал свою работу, игрок видит ошибку своего восприятия игры, не физики, а именно восприятия. Представьте самый обычный платформер, в котором персонаж бежит по платформе, приближается к краю и должен прыгнуть на следующую. Если вы сделаете правильную обработку прыжка в этом месте в стиле “персонаж стоит на земле - можно прыгать, персонаж на земле не стоит - прыгать нельзя”: if (IsGrounded() && JumpPressed()) then Jump(), то c точки зрения “корректной” физики здесь будет не к чему придраться, потому что если под ногами уже нет платформы было бы довольно странно сообщать персонажу, что он всё ещё находится на земле.
А если кнопка прыжка была нажата после того, как система коллизий говорит нам о потере контакта с поверхностью, то прыжок должен быть запрещён. Про такую логику можно даже сказать, что она ведёт себя “физически корректно”, но потом приходит тестировщик и говорит: “Ребят, прыжок какой-то странный. Я чувствую, что стою на земле, но прыжка нет”.
Вот это “чувствую” происходит из человеческого опыта, когда глаза у нас немного впереди, а пятки немного сзади и весь ваш предыдущий опыт говорит, что прыгнуть ещё можно. На самом деле нет, прыгнуть нельзя, стоя пятками на самом краю платформы прыгнуть вы не сможете, если вы, конечно, не Дрейк или Джеки Чан, ваш мозг просто компенсирует вашу позицию, относительно места прыжка, немного вам подвирая.
Ладно, оставим в покое тестировщиков, они люди добрые и часто жалеют игроков, а не разработчиков. Идём к нашему программисту физики и предъявляем ему жалобы игроков, на что получаем физически корректный ответ, неправильный, заметьте : «у нас отсутствует окно допустимого рассогласования между состоянием grounded и моментом получения jump input», переводим это на общечеловеческий язык “край платформы будет edge case, который не подчиняется физике”.
Где заканчивается платформа?

Допустим, персонаж бежит вправо со скоростью 5 метров в секунду, игра работает при 60 FPS, и в какой-то момент происходит следующее:
кадр 100 -> персонаж ещё на платформе кадр 101 -> персонаж ещё на платформе кадр 102 -> персонаж покинул платформу кадр 103 -> игрок нажал Jump
Т.е. для физики ситуация абсолютно однозначная, и на 103-м кадре персонаж находится в воздухе, поэтому IsGrounded() возвращает false, условие не выполняется, прыжка нет. Для человека перед монитором это воспринимается совершенно иначе, он ведь не видит отдельные кадры, а видит движение, причём видит его не как последовательность дискретных состояний, а как непрерывное событие. “Я подбежал к краю, и нахожусь буквально в нескольких пикселях от него, нажал кнопку, и с точки всё это произошло практически одновременно”.
Но игра уже успела разделить эту сцену на несколько совершенно разных состояний, записать между ними несколько миллисекунд разницы и как бы “поздно, земля ушла из под твоих ног”.
Здесь мы подходим к одной из фундаментальных для разработки игр проблеме, которая вообще не имеет отношения к прыжкам, просто там её видно чаще всего. Игра измеряет мир событиями, а человек воспринимает его состояниями движения, и то, что для игры произошло на 101-м, 102-м или 103-м кадре, для человека видится как “вижу край, решил прыгнуть и нажал кнопку”. Это не три разных действия, это одно из состояний движения. Эти два видения расходятся, и строгая логика начинает наказывать человека не за несовершенство его нервной системы.
И тут появляется койот из мультика

Сам термин “coyote time” отсылает к старому мультяшному гэгу с Wile E. Coyote, который выбегает за край обрыва и какое-то время продолжает уверенно бежать по воздуху, пока не смотрит вниз и не поймёт, что под ногами, собственно говоря, уже ничего нет. Что является довольно точной метафорой механики, когда персонаж уже покинул платформу и физически находится в воздухе, но игра ещё несколько кадров делает вид, что под ним земля и если игрок нажимает прыжок сразу после края платформы, игра всё равно позволяет ему прыгнуть.
if (IsGrounded() || timeSinceLeftGround < coyoteTime) Jump();
Вся механика в конечном итоге сводится к паре строк и конфигу, но мы только что специально добавили в игру баг, игра знает, что персонаж находится в воздухе, игра знает, что поверхность закончилась, игра знает, что игрок опоздал. Но мы вернули “правильное” поведение, которое ожидает игрок, который просто нажимает кнопку и видит прыжок. Хороший coyote time вообще не должен восприниматься как отдельная механика, и должен восприниматься как нормальное управление.

Кто вообще это придумал

Современное название явно связано с мультяшным койотом, однако определить первого человека, который придумал саму механику или первым назвал её именно coyote time, практически невозможно. Похожие варианты “прощения края” существовали в старых платформерах задолго до того, как современная терминология стала распространённой, а некоторые старые игры даже содержали явно похожие механики. В качестве примеров обсуждаются A Boy and His Blob, Donkey Kong Country, Super Mario Bros. 2 и другие игры конца 1980-х и 1990-х годов.
Coyote time очень похож на одну из тех идей, которые появляются сразу во всей игровой индустрии, когда десятки разработчиков сталкиваются с одной и той же проблемой, каждый немного по-своему её решает, а потом со временем сообщество находит удачное название для уже существующего приёма. Сначала появляется маленький костыль, потом костыль становится системой, потом система получает название, через двадцать лет разработчики спрашивают, какой float нужно поставить, чтобы получить “правильное” поведение. Но правильного float, конечно, не существует, потому что игроки нажимают на кнопки с разной задержкой, от 80 мс до 420 мс в худшем случае.
Зачем вообще нужен coyote time?
Тут надо на минуту забыть про прыжок, потому что, как я уже говорил, эта механика вообще не про прыжки, а про задержку между тем, что игрок видит и его реакцией. Человек видит платформу, оценивает расстояние, понимает, что сейчас нужно прыгнуть. Далее нервная система передаёт сигнал мышцам, палец начинает движение, кнопка нажимается, контроллер передаёт состояние игре, и только после этого наш код узнаёт, что произошло.
При этом игроку кажется, что он сделал всё мгновенно, для него нет разницы между «я нажал за 30 миллисекунд до края» и «я нажал за 30 миллисекунд после края», потому что эти события находятся практически в одной точке его субъективного времени. Для игры разница есть, потому что между моментом, когда некоторая система рапортнула, что действие невозможно, и нажатием кнопки может пройти до 30 кадров (в худшем случае 420 мс, что почти 30 кадров). Именно поэтому игры вообще вынуждены постоянно делать подобные послабления, проблема не в том, что игрок плохой или медленный, просто человеческое восприятие непрерывно, а игра в конечном счёте состоит из отдельных дискретных состояний.
И чем быстрее игра, тем сильнее эта разница начинает проявляться, и при 60 FPS один кадр длится около 16,7 миллисекунды, шесть кадров будет уже примерно 100 миллисекунд, что для человека практически верх быстрой реакции.
Обратная проблема coyote time

Ок, мы научились прощать игроку опоздание и теперь, пробежав край платформы и нажав прыжок через несколько кадров он всё равно прыгает, но появляется другая ситуация. Персонаж падает на платформу и хочет сделать “bunny hop”, т.е. продолжить движение серией прыжков, потому что видит, как персонаж коснулся земли, и нажимает кнопку. Но физика ещё не успела зарегистрировать приземление и получается:
кадр 200 -> персонаж в воздухе кадр 201 -> персонаж почти коснулся земли кадр 202 -> игрок нажал Jump кадр 203 -> персонаж приземлился
И снова игра права и на 202-м кадре прыгать еще нельзя, только теперь игрок опять считает, что сделал всё правильно. Он не хочет прыгнуть “сейчас”, а прыгнуть “как только приземлится” и если мы просто выбросим input, мы потеряем намерение игрока, выбив его из состояния движения, поэтому приходится делать вторую ошибку, так называемый jump buffer, где игра некоторое время хранит нажатие:
if (JumpPressed()) jumpBuffer = JumpBufferTime; else jumpBuffer -= dt;
А после приземления проверяем, не хотел ли игрок прыгнуть немного раньше:
if (IsGrounded() && jumpBuffer > 0.0f) Jump();

Это логично, и если Coyote time прощает опоздание, то некоторая система должна прощать поспешность. Что приводит нас к более точному описанию “правильного геймплея”, чем слово “отзывчивость”, но здесь я бы очень осторожно относился к самому числу, как вы понимаете, скорость реации у людей разная и если мы попробуем сделать магический параметр или параметры для обоих поблажек, то он всегда будет неправильным:
Coyote Time: Mario = 100 ms Celeste = 100 ms Hollow Knight = 120 ms Your Game = ????
Правильных чисел для такой системы нет, и 100 миллисекунд могут быть прекрасным значением для одного человека и недостаточным для другого, поэтому появляется ещё две системы, которые настраивает эти числа в зависимости от скорости персонажа, размера платформы, текущего ускорением, длительности прыжка, длительности текущей анимации, и прошлым временам нажатия игрока на кнопку, подводя нас к отдельной системе движения персонажа.
Как это влияет на сложность игры

Coyote time упрощает игру, игроку становится немного легче прыгать. И для двух игроков, которые по-разному понимают уровень и по-разному принимают решение, и нажимают кнопку на разных кадрах, игра убирает именно эту разницу. Это дает дизайнеру делать более сложные и зрелищные уровни, добавляя интересную сложность.
Но также современные игры без этой системы, теперь ощущаются сломанными. Когда игрок много лет играет в современные игры, он постепенно привыкает к определённому уровню “прощения”, не только в прыжках, этот coyote time протекает и в другие системы. Но теперь игрок даже не знает, что этот уровень существует, и он считается текущей нормой. Если он нажал кнопку прыжка чуть раньше приземления, персонаж должен прыгнуть. Если он только что сошёл с края, прыжок должен ещё сработать. Если он почти попал в угол платформы, игра, вероятно, должна как-то помочь. Если он начал действие на несколько миллисекунд раньше завершения предыдущего, игра, возможно, должна поставить input в очередь. Игрок не воспринимает это как помощь, и для него это теперь “нормальное управление”.
И когда он сталкивается с игрой, где ничего этого нет, возникает ситуация, что игра может быть технически “физически” корректной, но игрок говорит, что “Управление какое-то кривое”. Это не обязательно означает, что старые игры были плохими, просто изменился культурный стандарт того, как игра должна интерпретировать человеческое действие. Примерно то же самое произошло с игровыми сохранениями, когда-то было совершенно нормально потерять час прогресса после смерти, сегодня человек может потерять десять минут и уже начать подозревать, что разработчики решили провести над ним социальный эксперимент. Мы постепенно привыкаем к определённому уровню удобства, и после этого отсутствие привычного удобства начинает восприниматься уже не как отсутствие функции, а как “ошибка поведения программы”.
Хорошая игра иногда должна врать
Если игра может понять, что хотел сделать игрок, она должна постараться это сделать и тогда становится понятно, почему та же самая логика появляется далеко за пределами прыжков. И в современных играх эта система уже работает в буфере атак, дополнительные задержки для wall jump, системе позволяющей действию начаться чуть раньше завершения предыдущего, или сохранения импульса движения игрока ещё несколько кадров после того, как он остановился.
Мы признаём, что между намерением человека и состоянием симуляции существует небольшая временная и пространственная ошибка, а затем специально проектируем систему так, чтобы эта ошибка не превращалась в наказание. Мы привыкли думать, что хорошая физика максимально точно соблюдает заданные правила, но игроку не нужна ваша физика. Ему нужен персонаж, который делает то, что он от него ожидает.
Да, технически ты уже упал с платформы, но я понимаю, что ты хотел прыгнуть, поэтому давай считать, что ты ещё не упал, то это совершенно нормальная часть игровой разработки. Можно заставить человека подстроиться под компьютер, а можно немного подстроить игру под человека.
И если игра смогла подстроиться, то игрок даже не заметит, сколько маленьких правил вы нарушили ради него. Он просто нажмёт кнопку и персонаж прыгнет.
З.Ы. Все примеры тут

