Аим-ассист в мультиплеерном шутере — как сделать правильно
Аим-ассист — вечная тема для споров среди игроков. Для одних это узаконенный чит, для других — инструмент комфортной игры. Этическую сторону вопроса сейчас обсуждать не будем, просто примем факт, что большинству пользователей неудобно целиться на геймпадах и тачскринах. И это проблема.
Пару лет назад в нашем онлайн-шутере вообще не было аим-ассиста и автострельбы. Даже в сторе было написано, что Pixel Gun 3D — «hardcore shooter». Нас это устраивало, пока не стало сложнее привлекать новую аудиторию. Даже несмотря на обучение и матчмейкинг, новичкам было сложно освоиться.
На рынке тогда уже были шутеры с автодоводкой, но она была слишком навязчивая, чересчур влияла на игровой опыт и скилл игрока. Нам предстояло разработать систему получше — чтобы не разозлить бывалых хардкорщиков и не отпугнуть новичков. Рассказываю под катом, что из этого вышло.
Авто-аим и аим-ассист
Для начала на всякий случай определимся с терминами, чтобы не путать авто-аим и аим-ассист.
Авто-аим (автонаведение). Прицел жестко фиксируется на противнике, позволяя игроку расслабиться, зажать кнопку стрельбы и вообще не двигать стиком. Это, на удивление, до сих пор популярная система стрельбы, которая используется, например, в GTA V и Red Dead Redemption 2.
В мультиплеерных проектах подобное особенно фрустрирует. Почти всегда авто-аим можно отключить, но смысла в этом примерно никакого — у остальных игроков всегда будет преимущество.
Аим-ассист (помощь в прицеливании). Глобально он присутствует практически в любом шутере, даже на PC. Все эти маленькие хитрости разработчиков, когда пули примагничиваются к модельке противника даже если игрок на самом деле промазал на пару миллиметров, — тоже в каком-то смысле можно отнести к аим-ассисту. Например, такие лонгшоты в Battlefield были бы невозможны без помощи разработчиков:
Грамотно реализованную помощь в прицеливании пользователь может вообще не заметить. Система должна угадывать желание игрока — не выполнять что-то за него, а лишь помогать делать то, что он хочет. Именно с такими вводными мы начали думать над разработкой собственной системы помощи в прицеливании.
Выделили две концепции:
1. Когда прицел игрока попадает в область вокруг противника (обычно очень большую), то начинает примагничиваться к телу врага. Например, похожая система есть в Respawnables или Blitz Brigade на смартфонах.
Проблема в том, что она не всегда угадывает желание игрока. Допустим, он хотел перевести прицел влево, а рядом пробегал враг, «захватил» прицел и увел его в сторону. В итоге — неприятный игровой момент.
2. Другая концепция заключается в попытке предугадать поведение игрока с помощью считывания его действий. На ней и остановились.
Система отслеживает положение врага на экране и направление движения прицела игрока, пытаясь предугадать необходимые действия — провести прицел мимо или помочь навестись на цель.
Теперь подробнее о том, как это работает.
Как устроен аим-ассист
Окей, мы решили предугадывать желание игрока и помогать ему в прицеливании. Теперь предстояло определиться, как это лучше всего сделать.
Когда игрок наводится на противника, он направляет на него прицел и движением стика (физического или сенсорного) старается удержать цель в фокусе. Концепция выглядит не такой уж и сложной — мы определяем момент движения прицела к противнику и ненавязчиво помогаем игроку. Но дьявол кроется в деталях.
Путем проб и ошибок пришли к системе из двух зон вокруг персонажа: внешней и внутренней.
1. Внешняя зона отвечает за помощь в доводке камеры, но не с помощью притягивания прицела к врагу, а благодаря высчитыванию изменения положения курсора внутри ректа зоны.
Как это работает. Если прицел попал во внешнюю зону, а игрок движется вместе с противником, то его камера поворачивается так, чтобы прицел оставался на том же самом месте внутри этой зоны, где он был кадром ранее (плюс к этой позиции добавляется движение за счет инпута игрока). То есть мы не отбираем у игрока необходимость прицеливаться, а лишь помогаем его прицелу оставаться внутри внешней зоны.
Если резко включить ассист, то камера дернется и игрок воспримет это как вмешательство в управление. Поэтому при попадании прицела в эту зону включается специальный механизм приращения силы аим-ассиста. Назовем ее h:
h увеличивается со скоростью p, если внутри зоны прицел двигается к противнику;
h уменьшается со скоростью m, если движение идет в сторону от противника;
h уменьшается со временем.
Чтобы игрок мог легко убрать прицел из внешней зоны, m в 4 раза больше p (выведено эмпирически). Максимальное значение hозначает, что прицел железно закрепляется внутри ректа. Для поддержания этого значения нужно вести прицел к противнику, поэтому у нас нет ситуаций, когда игрок не ведет прицел к цели, но при этом чувствует какое-либо вмешательство. Дополнительно мы ограничили максимально разрешенную скорость доводки камеры и применили легкую интерполяцию.
Размер ректа на экране привязан к размеру противника, но не прямо пропорционально, а с поправкой на дистанцию. Даже если персонаж очень далеко, рект все равно будет занимать достаточную область экрана (если, конечно, игрок не вышел за максимальную дистанцию аим-ассиста).
Изначально наша помощь в прицеливании ограничивалась только внешней большой зоной, но этого было недостаточно. В Pixel Gun 3D персонажи перемещаются очень быстро, а интерполяция создает небольшое отставание помощи в прицеливании. Поэтому мы добавили внутрь вторую зону поменьше, которая всегда соответствует размеру персонажа на экране.
2. Внутренняя зона. В ней сила аим-ассиста h со временем не уменьшается, а растет. Также увеличивается максимальная разрешенная скорость доводки камеры (пока прицел находится внутри зоны).
Когда игрок удерживает прицел на враге, h достигает максимального значения — положение прицела «приклеивается» к внутренней зоне и меняется только инпутом самого игрока. При этом если прицел нужно отвести в сторону, то никакого сопротивления не будет за счет понижающего коэффициента m.
В итоге получилась незаметная система помощи в прицеливании, абсолютно лишенная ощущения, что у игрока выхватывают управление из рук:
доводка учитывает желания и подстраивается под пользователя;
при неподвижном прицеле ассист не включается;
при проведении прицела мимо врага нет сопротивления или замедления скорости камеры.
Это довольно общее описание работы аим-ассиста. Сейчас поговорим про нюансы.
Выбор цели
Неправильно выбранная цель может сильно подпортить игровой опыт и заруинить не один матч. Например, в Call of Duty Warzone встречается проблема, когда перестреливаешься с противником, и тут внезапно второй противник, пробежавший через линию огня, уводит прицел за собой, что приводит к плачевным последствиям и разбитым геймпадам. Забегая вперед — у себя мы решили эту проблему.
В нашем проекте много сущностей. Игроки и разные противники: турели, зомби, роботы, а еще есть разрушаемые объекты. Мы создали интерфейс IAimHelp и реализовали его для каждого уничтожаемого объекта — поэтому аим-ассист работает на все, что можно убить или уничтожить.
У каждой сущности, реализующей IAimHelp, для считывания попаданий есть один или несколько коллайдеров (хитбоксов), к которым мы напрямую привязали размер зон ассиста. Это решило кучу проблем с настройкой зон под каждую сущность.
Во время игры нужно понять, попал ли прицел игрока внутрь одной или нескольких внешних зон объекта, реализующего IAimHelp. Если выстраивать ректы каждый кадр для каждой сущности, то уйдет очень много ресурсов, поэтому сначала вычисляется вектор от игрока до сущности, после чего, используя скалярное произведение векторов, отсекаются все сущности, которые не попадают в область видимости игрока.
Отсечение при помощи скалярного произведения векторов:
private bool IsTargetInCameraCone(Vector3 IAimHelpPos,ref Vector3 deltaVector)
{
deltaVector = IAimHelpPos - myCamera.position;
Vector3 direction = (deltaVector).normalized;
return Vector3.Dot(direction, myCamera.direction) > 0.7f;
}
Затем для каждой сущности сохраняется дистанция и отсеивается все, что дальше максимально разрешенного расстояния для аим-ассиста.
Дальше пробрасывается луч в каждую из целей, чтобы определить, есть ли препятствие между игроком и целью — остаются только цели в прямой видимости игрока. И уже после этого высчитываются ректы.
Просчет ректа зоны для прицеливания:
public static Rect AimHelpRect(Collider c, AimSettings settingsContainer, float fieldOfView, float distanceToTarget , Camera mainCamera )
{
var bounds = new Bounds();
bounds = c.bounds;
Vector3 screenPoint = mainCamera.WorldToScreenPoint(bounds.center);
float width = bounds.size.x / GetSizeFactorByFOV(fieldOfView) / distanceToTarget * Screen.width * settingsContainer.width;
float height = bounds.size.y / GetSizeFactorByFOV(fieldOfView) / distanceToTarget * Screen.height * settingsContainer.height;
Rect newRect = new Rect(new Vector3(screenPoint.x, Screen.height - screenPoint.y) - new Vector3(width, height) / 2, new Vector2(width, height));
return newRect;
}
При построении ректа очень важен угол обзора, так как снайперский прицел работает за счет его сужения, поэтому ректы должны правильно перестраиваться.
Отсюда формула влияния поля зрения на размер ректа на экране:
public static float GetSizeFactorByFOV(float fov)
{
if (lastFov == fov)
{
return lastFovFactor;
}
float factor = 1;
float halffov = fov / 2;
float angleY = 180f - 90f - halffov;
factor = Mathf.Abs(Mathf.Sin(halffov * Mathf.Deg2Rad) / Mathf.Sin(angleY * Mathf.Deg2Rad) * 2);
lastFov = fov;
lastFovFactor = factor;
return factor;
}
Дальше идет проверка попадает ли прицел игрока внутрь ректа. И, если да, то цель становится текущей:
if (rect.Contains(AimSupportController.CenterOfScreen))
{
SetTarget(enemyTarget);
}
После этого у активной цели каждый кадр строится рект и проверяется видимость при помощи RayCast. Цель перестает быть активной, если RayCast не достигает ее или если прицел не находится в ректе дольше секунды.
Этот алгоритм работает, если у игрока еще нет ни одной цели. Другая ситуация — если игрок решит навестись на другого соперника, чей рект перекрыт ректом текущей цели.
В этом помогает проброска луча в центр экрана — каждые несколько кадров луч летит через все объекты по направлению обзора игрока, и если он дольше определенного времени попадает в другой объект, доступный для аим-ассиста, то текущей целью будет выбран именно этот объект.
В результате мы получили удобную систему выбора цели: активная цель не сбрасывается, если ненадолго пропадет из видимости, пробегающие мимо враги не перехватывают ассист на себя, а когда игрок специально наводится на другую цель, то аим-ассист переключается на нее.
Адаптивность
На силу помощи в прицеливании влияют сразу несколько факторов:
дистанция до цели;
уровень скилла игрока;
настройки оружия.
Насчет дистанции все ясно — чем дальше, тем меньше помощи. Но на деле все немного сложнее — чем дальше противник, тем меньше его хитбокс. Поэтому если понижать силу аим-ассиста линейно, то он будет бесполезен для дальних целей. Чтобы настроить фактор снижения силы помощи в зависимости от дистанции, мы использовали редактируемую кривую. В Unity это AnimationCurve.
Аим-ассист — дополнительная возможность комфортной игры для новичков, но в руках хардкорных геймеров — оружие массового поражения. Если новички еще изучают игровые механики и даже с ассистом не всегда попадают в цель, то игроки-ветераны будут выкашивать целые сервера, раздавая хедшоты направо и налево. Чтобы это сгладить, мы ввели адаптивную шкалу помощи в прицеливании в зависимости от опыта игрока.
Разумеется, аим-ассист ощущается иначе в зависимости от типа оружия. Например, пользы от него на пулемете с большим разбросом гораздо меньше, чем на ваншотной снайперской винтовке. В скором времени планируем это доработать и добавить для каждого оружия индивидуальные параметры помощи в прицеливании.
Как работает автострельба
Менее актуальная функция для PC и консолей, но часто необходимая в мобильных проектах — автострельба. Суть понятна из названия: когда игрок нацелился на игрока, то оружие автоматически начинает стрелять. Нужна она исключительно ради дополнительного удобства на мобильных девайсах, где нет физических кнопок.
Хотя умельцы из Китая уже понаделали множество специальных механических триггеров, которые крепятся на грань смартфонов и превращают его в подобие геймпада.
На AliExpress полным полно таких аксессуаров рублей за 200
Но мы все же исходим из того, что 99% игроков используют только сенсорный экран смартфона. Поэтому с автострельбой есть свои фишки. Ее, конечно, при желании тоже можно отключить.
В любом шутере есть несколько видов оружия: автоматы, пулеметы, снайперские винтовки, дробовики. Если разрешить мощной пушке стрелять сразу же после наведения прицела — появится проблема так называемых «хитсканов». Игрок возьмет ваншотную снайперку, будет хаотично водить пальцем по экрану и всех убивать. От игры с таким соперником мало удовольствия (вспоминаем видео с Red Dead Online выше).
Поэтому мы привязали момент начала автострельбы к скорострельности пушки — чем она выше, тем быстрее начнется автострельба. Например, на скорострельных автоматах задержка минимальная, а на снайперках — максимальная. При этом на урон мы не смотрим, так как он изначально привязан к скорострельности.
Таймер рассчитать довольно просто, хватает лишь минимальной и максимальной задержки перед автострельбой и скорострельности самой пушки, умноженной на определенный коэффициент.
С тем, когда включать автострельбу разобрались, но появилась другая проблема. Игрок навелся из автомата, началась автострельба, враг убегает и прицел соскакивает с него. Должна ли автострельба прекратиться? Нет. Игроки привыкли, что с автоматическим оружием нужно «зажимать», водить прицел и поливать врагов пулями — это привычный сценарий использования.
Чтобы реализовать этот сценарий, мы добавили еще один параметр — таймер прекращения автострельбы после потери цели. Например, у автоматов он долгий, чтобы можно было стрелять с «зажимом», а у снайперских винтовок — короткий, чтобы лишний раз не промазать. В обоих случаях это положительный игровой опыт.
Также важно, чтобы игроку было понятно, когда у него сработает автострельба. На автоматах она начинается почти сразу и понять это не сложно. Но на снайперках из-за высокой задержки ситуация другая, поэтому научить игрока поможет UI и специальная индикация. Например, мы добавили внешние элементы прицела — они сходятся вместе и сигнализируют о начале стрельбы. Кроме того, у пушек разная дальность и игрок должен понимать, начнется ли автострельба вообще, поэтому прицел имеет дополнительную цветовую индикацию (красный — автострельба возможна, белый — нет).
Реакция аудитории и метрики
Автодоводке и автострельбе в нашем проекте уже два года. Онлайн не упал, а жалоб от комьюнити практически не было. Немного были недовольны хардкорщики, которым не понравилось, что менее опытные игроки теперь получили шанс убивать.
При этом с самого выпуска апдейта, старых игроков на запуске приложения встречал экран, где объяснялось, что функции нужны для помощи новичкам. Прямо там же их можно было отключить не заходя в настройки. Это решение сильно помогло снизить негатив среди фанатов.
Если обратиться к метрикам, то распределение киллрейта игроков по квартилям поменялось. Игроков с очень низким киллрейтом среди новичков стало значительно меньше (как мы и хотели), но также значительно увеличилось количество игроков с высоким соотношением убийств к смертям на последних уровнях. Эту проблему уже позже решали настройкой значений задержки автострельбы и силы доводки в зависимости от уровня скилла.
Насколько же сильно помощь в прицеливании и автострельба повлияли на ключевые метрики (Retention, LT, ARPU) — однозначно сказать сложно. Они выходили вместе с батлроялем, о разработке которого я подробно рассказывал в двух статьях: про графическую и сетевую части.
В том же апдейте у нас впервые появились боевой пропуск, боты в матчах для новичков и многие другие фичи, да и сама воронка входа претерпела изменения. Вообще не стоит в одном апдейте смешивать много фич, направленных на одни и те же метрики, но порой приходится рисковать и ускоряться, чтобы сохранять должный уровень конкуренции. Самое главное, что совокупность всех усилий привела к тому, что метрики пошли вверх. Чего мы и добивались.