Хочу рассказать сообществу о проведённом мной эксперименте.
Мне всегда нравились игры, в которых есть физика. То есть, некоторые процессы не управляются скриптами, а эволюционируют во времени, следуя физическим законам. Из этого проистекают сложность и непредсказуемость игрового процесса.
Примеров много, физические элементы тонко пронизывают многообразие компьютерных игр. Взять хоть любой платформер: совсем другие ощущения от игры, когда есть инерция персонажа, скольжение, гравитация, урон от падения в большой высоты и отдача от оружия.
Или те же гоночки: до чего приятней на полной скорости сшибать людей, рекламные щиты и помойки, чтобы разлетались во все стороны, вместо того, чтобы мгновенно останавливаться, врезаясь в мёртво врощенный в землю столб.
Или ещё замечательный пример — Kerbal Space Program. Там физика уже является непосредственым источником геймплея.
Или, например, жанр 2д артиллерии. Часть его очарования базируется на уничтожаемой, динамичной земле. Но до чего он был бы лучше, если б земля не просто линейно осыпалась, а вела себя реалистично, разлетаясь от взрывов кусками.
Я давно мечтал сделать именно такой, до предела физически реалистичный римейк Scorched Earth. Но все мои эксперименты с моделированием физических систем упирались в неумолимо медленные процессоры. Тысяча-две частиц были пределом для real-time симуляции.
Но недавнее моё «открытие» изменило ситуацию. Мы живём во времена быстрого развития геймерского железа. Развивались главным образом видеокарточки, потому что производители игр с наибольшим энтузиазмом наращивали именно графическую составляющую игр, а производители железа поддерживали высокий fps. И не удивительно решение компании Nvidia добавить возможность для разработчиков писать код для графической системы, то есть осуществлять вычисления в графичесокм ядре.
По-моему, это решение было тихой революцией. Я конечно знал, что видеокарточка мощней процессора, но я не знал, до какой степени. В среднем геймерском компьютере производительность видокарты в 50-100 раз выше производительности центрального процессора.
Конечно, задача должна быть хорошо распараллеливаема, этот бонус актуален не для любого алгоритма. Но моделирование тысяч частиц идеально распараллеливается.
Осознав это, я понял, что наконец-то смогу создать полностью физическую игру, в которой не будет заскриптовано ничего, кроме физики. Игра станет эквивалентна физической симуляции.
Я давно делаю игры на Юнити, и был рад, узнав, что в этом движке реализован класс ComputeShader, который позволяет использовать в проекте шейдеры на языке HLSL. Просто пишем шейдер, линкуем его к экземпляру ComputeShader, и диспатчим в Update.
Вникнуть в параллельные вычисления на GPU было не слишком просто. Туториалов маловато, а те, что есть, довольно ограничены в объёме поясняемых тонкостей. Но ключевых трудностей было не так уж много, довольно богата справочная информация по HLSL была на msdn, так что кое-как путём проб и ошибок я овладел спецификой и начал делать игру.
Задача была проста: нужно смоделировать в реальном времени несколько десятков тысяч взаимодействующих частиц, и из них построить мир, который жила бы уже по своим законам.
Параллельные вычисления — коварная штука. Нужно было свести все вычисления к простым блокам одинакового размера, чтоб на каждую частицу приходился один поток. Я решил упростить до предела математику взаимодействия частиц. К примеру, обойтись без интегратора, просто мерять величину поля (посиывающего взаимодействие частиц) в текущей точке, и на его основе менять скорость частицы. Простота гарантировала, что все потоки будут выполняться одинаково быстро, и не возникнет тяжёлых потоков, которых остальными придётся дожидаться.
Кроме того, при взаимодействии частиц нужно было работать с данными в защищённом режиме, чтобы паралельные потоки были осведомлены об одновременной записи-чтении и не путались. Ведь если частица может одновременно взаимодействовать с десятком других частиц, все они могут параллельно изменять её скорость, а значит, необходимо делать это в защищённом режиме. Средства для этого в HLSL нашлись. Правда, операторы вроде InterlockedAdd() работают только с int-величинами, так что приходилось пожертвовать тоностью, и хранить скорость в видеопамяти в виде int-величин.
Огромные массивы взаимодействующих частиц — тоже коварная штука. Нужно было упростить сложность вычилений с
до чего-то вроде
. Этого я добился, создав двумерную сетку 256x256, и в каждом её элементе на каждом шагу сохранял ссылки на все ближайшие частицы, так что при обсчёте взаимодействия частиц, каждая частица взаимодействует лишь с теми частицами, что находятся в пределах нескольких 3x3 элементов сетки.
Кстати, почему вместо уже реализованного в Юнити физического движка я создал свой? Потому что универсальный физический движок, подходящий для широкого спектра задач, связанных с моделированием системы твёрдых тел, плохо подходит для моделирования системы взаимодействующих материальных точек. Я предпочёл оптимизированный под специфику оригинальный движок. Если создать в юнити тысячу объектов с rigidbody, можно убедиться, что fps падает довольно сильно. В моём случае нужны десятки тысяч частиц, и написанный с нуля движок позволяет их вычислять с хорошим fps.
Дискретная симуляция физического взаимодействия — пять же, коварная штука. Чем больше шаг, тем больше погрешность. Взаимодействия между частицами реализовано через силу Леннарда-Джонса, то есть, при сближении частиц сила отталкивания растёт в двеннадцатой степени. Это неимоверно усиливает ошибку, связанную с большим шагом. Попросту, материя взрывается, нарушается закон сохранения энергии.
Возникает противоречие: нам нужна быстрая real-time симуляция. Но шаг должен быть очень маленький. Это противоречие я разрешил уменьшив шаг в десять раз по сравнению с первыми экспериментами, и производя десять циклов симуляции в каждом Update(). Цена этого решения — производительность. Так что пришлось сильно уменьшить количество частиц. Но всё же их осталось достаточно для сложного поведения всей системы.
Ну, и ещё пара десятков хитростей были мной реализованы, чтоб получить удовлетворительное поведение материи. Например, я ввёл аналог валентных связей между частицами, которые распределяют импульсы и скорости частицы между соседями. Или, к примеру, гравитация не действует по всему объёму земли, а затрагивает только верхний слой частиц. Но если большие куски материи взлетят в воздух, гравитация будет на них влиять в полной мере. Это реализовано с помощью построения на каждом шагу гравитационной маски, и учёта её при расчётах.
И таких тонкостей ещё много, не хочу слишком углубляться в эту специфику. Месяца четыре в хобби-режиме ушло на решение всех проблем с параллельным вычислением и физикой, и в какой-то момент можно было переходить на уровень геймплея.
Что в итоге получилось? Получилась игра в жанре «2д артиллерия», вроде Pocket Tanks, Scorched Earth или Worms.
Вот короткая гифка, в которой показан игровой процесс:
imgur.com/gallery/Q5w8cjF.gif
А вот длинное, в десять минут, видео, в котором тоже показан игровой процесс, но более подробно:
Можно заметить, что земля и строения выглядят как желе. Это исправляется за производительности. Можно уменьшить шаг и усилить коэффициент влияния полей на скорость частиц. Но пока я стараюсь удержать игру на уровне не слишком обременительном для большинства не слишком старых видеокарт. Скажем, на моей карте GTX 750m, в которой 384 ядра, игра с 20 тысячами частиц работает с частотой 25 fps, что делает её вполне играбельной.
Вывода здесь два, объективный и субъективный:
1. Видеокарта обладает огромной мощностью, и сейчас уже не существует непреодолимых технических преград, мешающих разработчикам использовать её для вычислений. Это может открыть для практического использования прежде недоступные (из-за вычислительной тяжеловесности) подходы к игровому процессу.
2. Очень необычные ощущения возникают от игры в физически реалистичной песочнице. И по-моему, тут зарыто много неожиданных идей в области геймплей-дизайна. И хотелось бы, чтобы разработчики побольше экспериментировали с внедрением физики в геймплей, ибо на графике свет клином не сошёлся.
Автор: ThisIsZolden