Научитесь внедрять рандомизацию в создаваемую игру, чтобы игроки оставались вовлечены в процесс и стремились перейти к следующей сцене. Это второй пост Кристо Ноббса, посвященный разработке систем, основанный на авторском вкладе Кристо в книгу The Unity game designer playbook. В данной книге подробнее рассказано о том, как прототипировать, собирать и тестировать игровой процесс (геймплей) в Unity. В более раннем посте Кристо рассказывал, как разработчики могут создавать в своих играх такие системы, которые позволяют построить интригующий и неожиданный геймплей. Данный пост более узкоспециальный, рассказывает о том, как правильно организовать в игре рандомизацию. Можно простимулировать пользователей подробнее исследовать системы вашего игрового мира и самостоятельно получить уникальные визуальные впечатления, если расставить в игре визуальные подсказки. В более раннем посте Systems that create ecosystems: Emergent game design было описано пространство-песорчница, в котором все элементы сделаны из дерева, а также предусмотрена возможность неконтролируемого распространения огня. Давайте разовьем этот пример, и для этого предоставим пользователям возможность валить деревья топором. Предположим, что пользователь стоит в абсолютно плоском ландшафте и не знает, в какую сторону упадет срубленное дерево. Что, если бы среди деревьев попадался «сухостой» — то есть, мертвые, но до сих пор устойчивые стволы? Они могли бы падать в любой момент. Подобный непредсказуемый элемент в игре будет подзадоривать пользователя, и вся игровая среда станет восприниматься как более сочная.  Можно добавить сколько угодно визуальных подсказок, намекая пользователю, что ходить под деревьями в этом мире опасно, и в целом нужно быть начеку. Такие подсказки помогут игрокам заприметить отличия между сравнительно опасными мертвыми сухостойными деревьями – и теми, которые еще живы и здоровы. Игроку придется научиться взвешивать риски. Не будет ли разумнее собирать хворост возле уже упавших деревьев, ведь тогда меньше вероятность, что тебя травмирует еще одним упавшим деревом?   Как дизайнер, обязательно картируйте, где в лесу падающие деревья могут запускать такие системные цепные реакции. Если пользователь спровоцирует такую реакцию – развивайте ее, например, повышайте непредсказуемость падения деревьев, возгорания и при этом нарастания восхитительного хаоса. Скриптовый класс Random в Unity – это статический класс, предоставляющий вам подходы для генерирования случайных данных в игре. Есть одноименный класс System.Random в .NET Framework, и наш класс System.Random играет схожую роль, но по ключевым признакам отличается от System.Random из .NET Framework, в частности, в том, что он на 20% — 40% быстрее, чем исходный System.Random.  Ниже представлены статические свойства и методы, доступные в классе Random: insideUnitCircle: возвращает случайную точку в круге или на окружности радиусом 1.0 (только для чтения); insideUnitSphere: возвращает случайную точку внутри или на поверхности сферы  радиусом 1.0 (только для чтения); onUnitSphere: возвращает случайную точку на поверхности сферы радиусом 1.0 (только для чтения); Rotation: возвращает случайное вращение (только для чтения); rotationUniform: возвращает случайное вращение с равномерным распределением (только для чтения); state: возвращает или устанавливает полное внутреннее состояние генератора случайных чисел; value: возвращает случайное число с плавающей точкой в диапазоне [0.0..1.0] (включительно) (только      для чтения). ColorHSV: генерирует случайный оттенок на основе HSV и баланса белого; InitState: засевает исходное состояние генератора случайных чисел; Range: возвращает случайное число с плавающей точкой в диапазоне [minInclusive..maxInclusive](включительно). В более раннем посте автор рассматривал, какими рычагами располагает разработчик игры, и рассуждал о роли этих рычагов, а также об использовании объектов ScriptableObjects для хранения значений. Эти значения можно заменить тщательно подобранными диапазонами, воспользовавшись классом Random.Range из Unity, этот класс возвращает случайное число с плавающей точкой в диапазоне [minInclusive..maxInclusive] (включительно). Любое значение с плавающей точкой внутри этого диапазона, включая начальное и конечное, будет попадаться примерно один раз на каждые 10 миллионов случайных образцов. Такой подход позволяет вытянуть из заданного диапазона такое значение, которое даст искомый результат. Придется протестировать несколько диапазонов, чтобы найти тот, который будет отвечать целям вашего геймплея – но, опять же, убедитесь, что проектируете игру в рамках установленного вами диапазона значений. Фактор случайности способствует погружению в игру. Допустим, что у каждого дерева есть фиксированный запас здоровья, равный 100, и что каждый удар топора отбирает у дерева 25 баллов здоровья. Такая ситуация вскоре станет предсказуемой и, следовательно, наскучит. Даже если вы присвоите деревьям диапазон здоровья от 76 до 100, то любое дерево можно будет свалить не более чем с четырех ударов. Но, если задать меньший диапазон значений, скажем, от 75 до 76, то результаты геймплея приобретут значительно большую вариативность, поскольку, чтобы срубить дерево, по нему нужно будет рубануть от трех до четырех раз. Еще один вариант, позволяющий повысить занимательность такого сценария – обозначить изменение жизнеспособности дерева не схематическими «полосками», а четкими визуальными подсказками. В таком случае пользователь прямо в процессе игры усвоит, сколько примерно ударов топором требуется для того, чтобы свалить дерево. Визуальные подсказки привносят некоторую ограниченную непредсказуемость, которую можно сбалансировать и уравновесить с учетом целевого геймплея. Если работать с классом Random, а не с фиксированными значениями, то можно превратить монотонный процесс в увлекательный. Вот как можно расширить этот процесс: на ваш выбор, каждый удар топора может отбирать у дерева от 15 до 25 очков здоровья. В таком случае пользователю будет не так легко спрогнозировать, сколько ударов нужно, чтобы свалить дерево. Приходится тщательнее присматриваться к визуальным подсказкам, чтобы судить о том, когда упадет дерево – например, насколько крупные щепки от него летят, есть ли на стволе широкие трещины, падают ли ветки, ориентироваться на звуковые эффекты и т.д. Игрок никогда не сможет с точностью определить, в какой именно момент упадет дерево, но, превращаясь в заправского лесоруба, игрок может делать обоснованные догадки, что, в конечном итоге, повышает его шансы на выживание. В книгах по разработке на Unity приводятся примеры, как создавать такие рычаги и визуализировать поля в «Инспекторе» (Inspector). На этой картинке «Damage Intensity» (Интенсивность урона) – это рандомизированное значение, влияющее на окончательное «Damage Value» (Значение урона) – отображается с атрибутом  Range (диапазон)  Фактор случайности призван ставить перед игроками непредсказуемые проблемы, что вынуждает пользователя просчитывать риски и управлять результатами.   Давайте еще на нескольких примерах рассмотрим, как работать с классом Random. Представьте себе карточную игру, в котором вашим соперником выступает искусственный интеллект, и его ходы – это целиком и полностью реакция на то, как ходите вы. Без фактора случайности такое занятие вскоре станет предсказуемым, поскольку вы раз за разом будете приходить к одним и тем же результатам. Даже если установить значение случайности на 50%, то такая рандомизация будет слишком простой, и в скором времени станет для пользователя очевидной. Вместо этого попробуйте реализовать многоуровневую случайность, основываясь на действиях игрока. В таком случае у вас получатся затейливые последовательности, которые позволят действовать более динамично, нежели выбирать две карты из пула, либо не ограничиваться «выберем эту карту из множества равноценных, имеющихся в актуальном пуле». Можно добавлять в таблицу карт уровни сложности, сделав так, чтобы соперник предпочитал одни карты другим; решения зависели от того, какое значение ему выдано, либо, например, насколько полна его раздача перед атакующим ходом. Карта с картинкой могла бы давать кратный ущерб, если бы использовалась в комбинации с другими старшими картами. Таким образом, искусственный интеллект дожидался бы, пока у него на руках соберется определенная комбинация сильных карт – тем самым удавалось бы увеличить сложность игры для человека. Каждой такой комбинации можно присвоить «вес», увеличивая или уменьшая вероятность того, что старшая карта будет разыграна в комбинации с определенными мощными картами. Фактор случайности в играх может быть реализован и в других формах. Например, шум Перлина  обладает естественными свойствами и генерирует градиентный шум на основе исходного значения. Попробуйте воспользоваться им в Cinemachine, чтобы запрограммировать более органичную «операторскую работу», как будто съемка ведется «со стороны».  Чтобы попробовать на практике шум Перлина, познакомьтесь с комплектами Starter Assets – Third-person Character Controller или Gaia в «хранилище ресурсов» (Asset Store), а также почитайте документацию о том, как работать с Mathf.PerlinNoise. В одном интервью Крис Батчер, один из ведущих инженеров, работавших над игрой Halo 2 от Bungie Studios, рассуждая об игровом ИИ, говорит: «Цель – не создать нечто непредсказуемое. Нам нужен самосогласованный искусственный интеллект, предсказуемо реагирующий на тот или иной пользовательский ввод». Игрок может совершать поступки и рассчитывать, что искусственный интеллект отреагирует на них так, а не иначе».  С учетом изложенного, какие настройки следует задать для ИИ-агентов, чтобы ваша игра воспринималась непредсказуемо и живо? В качестве одного из экспериментов, попробуйте скомбинировать Starter Assets с инструментами реализации ИИ, предлагаемыми в хранилище ресурсов (Asset Store), например, с A* Pathfinding Project Pro, инструментом, позволяющим переместить агента в заданную точку.  Когда ИИ-агент приближается к игроку, игрок воспринимает это как подготовку к нападению. Но что, если персонаж собирается с ним просто поговорить? Как насчет добавить побольше неигровых персонажей (NPC), которые ходят вокруг и воспринимаются как фоновые элементы, оживляющие всю сцену? Эти персонажи могут выбирать точки, выдаваемые им линейно, но еще лучше – если они будут выбирать логические точки, основываясь на наборе правил, заданных в классе Random. Допустим, у вас есть ИИ-агент, выпускающий в игрока стрелу из слабого лука. К сожалению для агента, он должен подобраться к персонажу на определенное расстояние, поскольку максимальная дальнобойность его стрел составляет 10 метров. ИИ занимает позицию напротив игрока, в 10 метрах – и стреляет. Не самая интересная расстановка, особенно, если вы добавите второго лучника, который станет бороться с первым именно за эту позицию в сетке NavMesh. Чтобы получилось интереснее, выберите ту область, в которой враг должен подбираться к игроку. Сделайте это при помощи Random.insideUnitCircle, передав результат vector2 в vector3 для осей X и Z, а затем задействовав RandomRange для обоих, чтобы получить область с максимальным и минимальным радиусом вокруг игрока. ИИ-агент должен выбрать точку в рамках допустимого диапазона вокруг игрока и выстрелить, а не подбираться к игроку еще ближе. Чтобы добавить в игру еще огонька, обходясь минимальным кодом, примените это правило ко всем агентам ИИ, так, чтобы они могли атаковать игрока под разными углами. Действия ИИ будут предсказуемы лишь до некоторой степени, поскольку вы знаете: агенты могут на вас напасть, лишь будучи на некотором удалении от вас, но вы не можете спрогнозировать, какой угол атаки они выберут. Можно развить этот пример, позволив ИИ-агентам нападать навалом. Точно так же выбирайте случайную точку рядом с пользователем, но уменьшите радиус. В таком случае ИИ-агенты будут выбирать из пула возможных атак, когда представится момент для нападения. Игрок знает, что на него могут напасть толпой, но с какой стороны? Ожидать ли ему сокрушающего удара сверху? Либо размашистого в горизонтальной плоскости? Придется присматриваться к анимации, чтобы понять, атаку какого рода придется встречать. Этот сценарий может усложниться, если у вас есть множество вражеских NPC, которые, по сценарию игры, должны нападать на главного героя одновременно. Но, если взять для примера Far Cry 2 от Ubisoft, то там видно, что все враги сразу не нападают на главного героя. Различные враги нападают в разные моменты, случайным образом. Этот пример и другие сценарии работы с ИИ подробнее разобраны в этом видео от Game Maker Toolkit. Если вы попытаетесь реалистично воспроизвести в вашей игре результаты, полученные в натурных испытаниях, для этого придется писать сложные многогранные уравнения или применять продвинутое машинное обучение. В конце концов, результат все равно может получиться карикатурным, а, чтобы его добиться – вам придется потратить массу времени и пойти на технические издержки. Даже в таком случае данную систему будет сложно держать в сбалансированном виде, если она будет плохо продумана с архитектурной точки зрения.   Но при помощи класса Random из Unity разработчики игр могут создавать правдоподобные результаты для любых сцен, тонко контролируя заинтересованность игрока, причем, добившись этого за сравнительно краткое время.  Естественно, фактор случайности – решение не на любой случай. Но, пожалуй, этот фактор всегда остается существенным. Если же применять его там, где уместно, причем, в нужные моменты, то можно подарить пользователю уникальные ощущения, которые заставят его возвращаться к вашей игре снова и снова.