
Ключевой компонент игры Getting Over It with Bennett Foddy — это молот главного персонажа. Именно с помощью этого приспособления игрок передвигается в окружающем пространстве и пытается взобраться на гору, а затем всё более абстрактную череду нагромождённых объектов. На днях разработчик в общих чертах объяснил принципы работы этой детали игры.
Сайт Reddit.com разбит на многочисленные сообщества, называемые подреддитами. Полностью следуя принципам создаваемого пользователями контента, площадка позволяет любому желающему не только опубликовать пост или комментарии под ним, но и создать собственный подреддит, а затем самостоятельно регулировать его развитие и управлять сформированным сообществом. С января 2008 года, когда на Reddit открыли регистрацию сообществ всем желающим, пользователи создали более 100 тысяч активных подреддитов. Их тематика варьируется от общераспространённых (фотографии котов, порнография, форумы различной направленности) до крайне узкоспециализированных.
Типичный представитель последней категории — это сообщество /r/howdidtheycodeit. Как нетрудно догадаться по его названию, этот подреддит предназначен прежде всего для начинающих и разработчиков среднего уровня, которые заметили где-то интересную программную механику, но не могут разобраться, с какой стороны подступиться к её реализации. Участники разбирают возможную структуру алгоритма, предлагают прототипы и обсуждают компромиссы. Допускается выдвижение собственных гипотез, то есть объяснений иных способов получения того же результата.
Как и для любого другого подреддита, оценить тематику /r/howdidtheycodeit легко по самым популярным постам. До недавнего времени наибольшим рейтингом обладала публикация двухлетней давности про реализацию необычного эффекта WebGL, который умеет получать данные о положении одного окна браузера или позициях нескольких окон на рабочем столе. В комментариях указали, что это некая комбинация window.getScreenDetails() для информации об экранах и localStorage для примитивного канала обмена между несколькими окнами одной и то же страницы. Также возможно, что каждое окно узнаёт собственные координаты и размеры через window.screenLeft, window.screenTop, window.innerWidth, window.innerHeight и записывает всё в localStorage, а поведение нескольких окон координирует через событие storage.
Вчера на первую строчку самых популярных публикаций подреддита взобралась новая: реддитор рассказал, как пытался повторить механику молотка из Getting Over It with Benett Foddy, и попросил помощи. В комментариях неожиданно появился создатель игры.
Беннетт Фодди не просто так включил своё имя в название — во время геймплея его голос часто комментирует успехи и неудачи игрока. Независимый гейм-дизайнер сам мог рассказать и о том, и о другом.
На момент выхода продукта в октябре 2017 года Фодди уже успел получить широкую известность за счёт QWOP, игры, где преодоление спринтерской дистанции превращено в сложную механику по управлению ногами бегуна. Мышцами ног предлагалось управлять нажатиями одноимённых клавиш на клавиатуре, и адаптироваться к подобному сложно. Браузерка 2008 года имела огромную популярность: к августу 2011 сайт Беннетта посетили 30 миллионов раз.
Менее известно, что Фодди неоднократно пытался повторить успех QWOP, пусть и популярности последователи снискали меньше. В CLOP бегает единорог, а в GIRP по вертикальной поверхности карабкается скалолаз с обнажённым торсом. Эта тематика до конца развита в Getting Over It, где игрок берёт на себя роль полностью обнажённого (на это указывает пароль для старых версий) мужчины Диогена, тёзки древнегреческого философа, сидящего в огромном котле и держащего в руках скальный молоток с длинной ручкой.
Если для предыдущих браузерок нужна клавиатура, а в физическом поединке Get On Top один аналоговый стик управляет ногами персонажа, второй — руками, то Getting Over It проходится с помощью мышки или тачпада. Диоген передвигается в двумерном игровом пространстве с помощью молота, отталкиваясь от земли и цепляясь за многочисленные объекты из нагромождений камней, мебели, строительных конструкций и других неожиданных объектов.
Положением молота игрок управляет движениями мыши, что допускает произвольно резкие взмахи. Тем не менее игра представляет большую сложность, поскольку для перехода от непредсказуемого результата до сформированного моторного навыка нужно время и усилия. Поскольку это центральный элемент геймплея, точная механика работы молота — это самый важный элемент дизайна игры.
Возможно, что в комментариях к вопросу на Reddit отметились бы только немногочисленные клоны Getting Over It, к примеру, Getting Over Goombas, однако внезапно некто foddydotnet написал длинное сообщение. Беннетт Фодди лично пояснил механику молота и даже показал фрагмент исходного кода игры. При этом всех исходников разработчик не выложил, опубликована лишь функция fixedupdate. Также не представлен весь класс персонажа или версия сборки.
Фодди начал с оговорки, что спустя столько лет уже не помнит устройство игры во всех подробностях. Основная схема выглядит следующим образом: обратная кинематика рук не участвует в физике, молот управляется вращательным и поступательным сочленениями, рукоять собрана из нескольких тел, а котёл соединён с невращающимся корнем отдельным шарниром. Систему Беннетт называет простым пропорционально-дифференциальным регулятором.
Vector2 mw = (Vector2)fakeCursorRB.position;//this value ranges between 0.1 and 0.4mouseVelocityAverage =Mathf.Lerp(mouseVelocityAverage, Mathf.Max(0.1f * mouseInput.magnitude, 0.001f), 0.05f) + 0.005f;//move fake cursor - using accelerationVector2 newPosition = fakeCursorRB.position + mouseInput;//* mouseVelocityAverage;//clamp to radiusfloat distanceToPlayer = ((Vector2) transform.position - newPosition).magnitude;if (distanceToPlayer > 3.5f){ newPosition = (Vector2) transform.position + ((newPosition - (Vector2) transform.position).normalized * 3.5f);}Vector2 tipDelta = (Vector2) tipRB.position - mw;Vector2 poleDir = (Vector2) (tipRB.position - hjRB.position).normalized; //hjRB is the hinge joint RB//a dot product is used to make the rotation dominate when the mouse is perpendicular to the poledotProd = Vector2.Dot(poleDir, tipDelta.normalized);posDelta = tipDelta.magnitude * dotProd; //move back to tip headnewPosition += 0.5f * tipDelta * Mathf.Clamp(0.2f - Mathf.Min(mouseVelocityAverage,0.2f), 0, 0.2f); fakeCursorRB.MovePosition (newPosition);fakeCursorRB.velocity = (newPosition - fakeCursorRB.position);Vector2 mwDelta = (Vector2)hjRB.position-mw;Vector2 tDelta = hjRB.position - tipRB.position;float angleDelta = Vector2.SignedAngle(tDelta,mwDelta); //hingeJoint2D.angle no longer works accurately//different behavior close vs farangleDelta = Mathf.Sign(angleDelta) * Mathf.Max(Mathf.Abs(angleDelta/2f), Mathf.Abs(angleDelta * angleDelta));angleDelta *= Mathf.Clamp01 (Mathf.Abs (angleDelta) / angleEpsilon);motor.motorSpeed = Mathf.Clamp( (angleDelta * (3f) + (angleDelta - oldAngle) * 0.00f) * Mathf.Pow(Mathf.Clamp01(mwDelta.magnitude/deadzone),2), -800f,800f);hj.motor = motor;float speedFactor = 16f - Mathf.Max(sj.reactionTorque*0.008f ,5f); //had to make this stronger, no idea whyfloat axisBlend = Mathf.Pow(dotProd,4);slider.motorSpeed = Mathf.Clamp(-posDelta*Mathf.Abs(posDelta) * speedFactor * axisBlend,-50f,50f);sj.motor = slider;oldAngle = angleDelta;
Как можно догадаться, в игре не задействована позиция системного курсора, в сцене есть fakeCursorRB. При этом (фрагмент кода ниже комментария \\сlamp to radius) курсор накапливает относительное движение мыши, но не может удалиться от персонажа более чем на 3,5 единицы. Также подмена курсора решает проблему движения камеры: если в каждом кадре переводить абсолютную позицию мыши из координат экрана в позицию молота, то сдвиг камеры сам по себе будет двигать молот. Фодди упоминает некую логику компенсации, которая отделяет движение камеры от намерения игрока, но этот код не приводит.
Также по фрагменту Mathf.Pow(Mathf.Clamp01(mwDelta.magnitude/deadzone),2) очевидно, что Беннетт не забыл, насколько плохо направление на курсор определено в центре вращения. Небольшое линейное движение мыши в этой области может вызывать большое изменение вычисленного угла, поэтому в самом центре вращение почти не запрашивается, и лишь по мере удаления от центра чувствительность квадратично растёт. Однако по достижении радиуса deadzone множитель становится равным единице и больше не увеличивается.
Неожиданно, но физика работает быстрее частоты обновления типичного монитора. Беглый поиск обнаруживает, что в Unity функция обновления физики FixedUpdate по умолчанию вызывается каждые 0,02 с, то есть 50 раз в секунду, но Беннетту и 60 Гц было недостаточно. Если судить по его комментарию, он использовал continuous collision detection с множеством подшагов. Вероятно, что сделать так пришлось, поскольку код задаёт целевую угловую скорость вплоть до 800 градусов в секунду и целевую линейную скорость вплоть до 50 единиц в секунду.
Корневое тело может перемещаться по сцене, но вращение заблокировано. В Unity запрет вращения удаляет соответствующую степень свободы тела. Если бы этого сделано не было, то реактивный момент от молота преимущественно раскручивал бы всю конструкцию вместо относительно контролируемого поступательного движения. Если верить Фодди, в остальном физика работает по-честному, без каких-либо скрытых волшебных сил из ниоткуда.
Диоген сидит в котле, который закреплён к корневому телу на hinge joint 2D с ограниченным диапазоном углов. За счёт этого видимая часть персонажа может раскачиваться и реагировать на многочисленные столкновения.
Как объясняет Фодди, между поступательным телом и собственно головкой молота находится несколько прямоугольных физических тел, изображающих рукоять. Для их сочленения используется fixed joint 2D для максимальной, но не абсолютной сварки, чтобы некая податливость всё равно оставалась. А вот руки — чистая обратная кинематика, которая к рукояти никаких сил не прикладывает: физическая система определяет положение молота, а затем подбираются позы плеч и локтей так, чтобы ладони оказались на рукояти.
Расстояние и угол между наконечником молота и мышью используются, чтобы задавать моторы шарнирного и поступательного сочленений. Производится это с помощью того, что Беннетт называет пропорционально-дифференциальным регулятором: скорость каждого мотора зависит от того, насколько система отклонилась от целевого угла и вылета, но уменьшается с учётом скорости движения сочленения.
Интересно, что опубликованный фрагмент кода не полностью соответствует этому описанию. В формуле для вращательного мотора потенциально дифференциальная составляющая angleDelta - oldAngle умножена на 0.00f, а в расчёте скорости поступательного мотора отдельной производной вообще не видно. Поэтому по показанному коду уверенно можно говорить лишь о нелинейном преобразовании ошибки положения в целевую скорость моторов. Но вообще-то Фодди предупреждает, что с этим кодом он много экспериментировал и дорабатывал его эмпирическим путём, чтобы достичь максимально приятного физического поведения, и именно поэтому многие компоненты умножены на ноль или возведены в какую-то степень.
Действительно, это заметно по тому, как в коде определяется motorSpeed. Здесь мышь не задаёт ни приложенную к котлу силу, ни новое положение наконечника молота, она определяет цель, к которой два физических привода пытаются приблизить молот.
motor.motorSpeed = Mathf.Clamp( (angleDelta * (3f) + (angleDelta - oldAngle) * 0.00f) * Mathf.Pow(Mathf.Clamp01(mwDelta.magnitude/deadzone),2), -800f,800f);hj.motor = motor;
slider.motorSpeed = Mathf.Clamp(-posDelta*Mathf.Abs(posDelta) * speedFactor * axisBlend,-50f,50f);sj.motor = slider;
Для вращательного сочленения задаётся целевая угловая скорость, а для поступательного — целевая линейная скорость. Фактическое движение затем рассчитывает физический движок.
Фодди не расписывает отдельно, что именно происходит внутри физического движка в момент столкновения головки молота с уровнем. Он формулирует устройство системы проще: игра почти не определяет физику состояния вроде нахождения молота на земле, и обнаружение таких состояний требуется в основном для звуков и частиц. Действительно, в опубликованном коде нет проверки столкновений с землёй или касания молота поверхности.
Согласно документации Unity, мотор hinge joint 2D прикладывает ограниченный момент, пытаясь достичь заданной угловой скорости, а мотор slider joint 2D прикладывает ограниченную силу, пытаясь достичь заданной линейной скорости вдоль оси сочленения. Оба компонента связывают физические тела, поэтому прикладываемые ими усилия участвуют в движении всей соединённой конструкции.
Нетрудно восстановить причину перемещения персонажа: если столкновение с уровнем мешает головке молота двигаться так, как требуют заданные скорости моторов, физический движок рассчитывает движение остальных связанных тел с учётом сочленений и контакта. Корневое тело не может вращаться, но может перемещаться вместе с котлом. В результате голый мужчина в чугунном котле взмывает ввысь.
В комментариях Беннетта также спросили, почему он описывает игровую логику в терминах ПИД- и ПД-регуляторов — не занимался ли он реальными двигателями? Игразработчик ответил, что хватило бакалавриата по физике, который он закончил в среднем на четвёрки.
ссылка на оригинал статьи https://habr.com/ru/articles/1058204/