У меня за плечами несколько лет работы в IT, но в сферах, связанных с геймдевом, я никогда не работал. Но это не помешало мне поучаствовать в Tech Jam от Facepunch для ещё не вышедшей s&box. О том, как это было (и обо всех провалах в процессе), я и решил написать статью.
О себе
Я периодически пишу в блог про ООП и статический анализ (в основном про ту часть, что связана с компиляторами). Как можно заметить, игр в этом списке нет. Всё потому, что мой профессиональный опыт ограничивается вебом и тем самым статическим анализом.
Хотя с разработкой игр я всё же немного знаком. Начинал я с редактора Warcraft 3, а также потратил немало времени на аддоны для Garry’s Mod. И это не считая многих ушедших в стол небольших игр на Love2D.
Тем не менее, сколько-то компетентным в вопросе меня назвать сложно, поэтому приключение выдалось насыщенным.
О s&box
Если вы не слышали, то не так давно были новости, что Garry’s Mod является самым продаваемым ПК эксклюзивом. И спустя почти 20 лет к выходу движется его духовный наследник - s&box. Игра шагнула дальше своего предшественника, целясь во что-то между Gmod, Roblox и, внезапно, Unity.
Для истории важно, что в комплекте идёт платформа для разработки, которая является надстройкой над Source 2. В неё входит собственный редактор игры, редактор уровней для Source 2 (Hammer Editor), база ассетов и API на C#. То есть да, с Unity много общего.
После нескольких лет закрытого теста, летом 2024 s&box стал открыт для всех желающих (для получения доступа надо было лишь запросить игру на сайте), и в полуоткрытом тесте игра находится до сих пор. Это означает, что многих функций ещё нет, а API достаточно нестабилен.
За всё время разработчики успели провести два геймджема, и сейчас объявили джем технический. Участвовать я пытался ещё в прошлый раз, но тогда не осилил кривую сложности. После я занимался игрой-головоломкой, а сейчас решил принять участие.
Идея
Любой геймджем начинается с хорошей идеи. Поначалу мне хотелось сделать какую-то очень упрощённую версию космического движка (не то KSP, не то Space Engineers), но, к счастью, эту идею я быстро отмёл из-за сложности.
Так что я принялся думать, что я вообще смогу осилить. Так как я являюсь любителем одной известной серии игр про выживание, аномалии и охоту за артефактами, мне пришла простая идея: сделать ИИ для ботов, которые смогли бы тебя окружать. В игре это обычно сопровождается запоминающимся криком “Сбоку, сбоку заходи!”.
Так и родилось название для моего проекта - SbokuBot. Являлось ли ошибкой, что я не назвал его s&boku bot, воспользовавшись очевидным каламбуром, я думаю до сих пор.
Мне казалось, что это удачный выбор, ведь игровой ИИ часто строится на основе графов, а это уже ближе к моим профессиональным навыкам (совсем недавно писал об этом статью). Первоначальная идея звучала как-то так:
Сделать ботов, которые будут тебя окружать, активно используя при этом укрытия. Они будут прятаться за ними пока перезаряжаются, а перемещаться между ними перебежками, пока их союзники отвлекают на себя внимание.
Тогда же я начал прикидывать, как это вообще можно реализовать. В s&box уже есть NavMesh с API, который позволяет строить по нему пути. Самым сложным тогда казался вопрос укрытий. Как их определять? Вручную? Сканировать местность и запоминать? Выводить из геометрии?
Сначала я хотел сделать это при помощи весового NavMesh, но впервые открыв API reference, я был удивлён его минималистичностью. Максимум, что с ним можно сделать - это получить путь из точки A в точку B. Из-за этого как-то уточнить идею не вышло, так что пришлось действовать исходя из довольно общего описания задачи.
Начало разработки
Далее встал вопрос, с чего вообще начать. Так как в своей основе s&box - это почти тот же самый Unity, где всё, включая контроллер персонажа, надо делать с нуля, базироваться на чём-то было скорее необходимостью.
Сами Facepunch сделали свой шутер Nicked, который я и хотел изначально форкнуть. Благо, у него открытый исходный код.
Но немного почитав обсуждения, я отказался от этой мысли. Всё-таки он слишком тяжеловесный, и лучше было бы использовать что-то более простое. Так я и вышел на Simple Weapon Base. SWB - это оружейный пак с демонстрационным FFA Deathmatch режимом в комплекте. Словом, ровно то, что мне было нужно.
Какова была моя радость, когда я увидел, что там уже есть возможность спавна ботов-пустышек! Всё словно уже было готово к тому, чтобы я просто взял и добавил им мозги. Как бы не так.
Simple Weapon Base
Итак, я начал изучать внутренности SWB. Не описать моё удивление, когда я увидел в классе `PlayerBase` следующее свойство:
[Sync] public bool IsBot { get; set; }
То есть сущность бота определялась не отдельным классом, а всего лишь свойством. Соответственно везде, где требовалось своё поведение, стояли if
-ы. Тут у меня закралось первое подозрение, что всё может оказаться немного сложнее, чем я рассчитывал.
И так оно и вышло. Видимо, в SWB не предусматривались боты совсем, из-за чего в классе оружия проверка на попытку перезарядится происходила так:
else if ( Input.Down( InputButtonHelper.Reload ) )
То есть в самом классе оружия велась проверка на то, что человек нажал кнопку. И со всеми остальными действиями типа стрельбы была такая же ситуация. Это приводило к таким забавным ситуациям, что если дать боту оружие и зажать ЛКМ, то стрелять будешь не только ты, но и он (ведь компонент, который висит на его оружии, слушает твои нажатия на клавиатуру).
Если проблему со свойством вместо класса я решил созданием своего контроллера, реализующего общий интерфейс, то тут пришлось уже влезать в код проекта и рефакторить, дополняя тот самый общий интерфейс.
Прозвучало, на самом деле, как не самая большая проблема, но на этой ступеньке я встрял почти на неделю - обилие новой информации не давало быстро продвигаться. Всё же если не знать достаточно хорошо линейную алгебру, то даже тривиальные задачи в духе “повернуть камеру бота в направлении объекта” ужасают. Я, когда по пути узнал про кватернионы, скорее всего, немного поседел.
И тем не менее где-то с четвёртой попытки у меня всё же получилось сделать новый класс для бота, и дело пошло дальше.
Эксперименты с NavMesh
Решив срочные вопросы с основой, пора было (наконец-то) переходить к ИИ. API уже предоставляет компонент NavMeshAgent
, который умеет находить пути к поставленной цели и может не встревать в других агентах, но я упёрся в одну комичную проблему. Агентов постоянно заносило, будто им не хватает трения.
Прошерстив дискорд в поисках советов, я не нашёл ничего лучше, чем и агента своего создать с нуля, работая напрямую с API NavMesh. Задача, в целом, несложная: просто получить путь из GetSimplePath
и сохранить его, пока не потребуется обновить. Пришлось только пожертвовать контролем толпы.
Где-то за день у меня получилось сделать ИИ, который может за тобой бегать и стрелять.
Ещё одна ремарка. Прыгать с места в карьер я не стал, и вместо этого решил начать с чего попроще. Мне пришло в голову смимикрировать поведение NPC в Half-Life 2 - они никогда не казались мне особенно умными, но и как столбы никогда не стоят - либо просто делают случайные перемещения, либо прячутся за укрытиями. Так что это стало по совместительству и моей первой ступенькой, и вариантом “минимум”.
Конечный автомат
Немного рассуждений о теории
Где-то через неделю после начала работы я, наконец, взялся за что-то, напрямую связанное с ИИ. И здесь же плотно встал вопрос: каким образом это реализовать?
Быстрый поиск показал три варианта:
Конечный автомат, где бот переключается между разными состояниями (меняет свой алгоритм действий целиком).
Поведенческое дерево.
Дерево решений.
И я не просто так не дал никакой ремарки касательно второго и третьего варианта. Я их не понял :). В то время, как конечный автомат выглядит как частный случай известного мне паттерна состояние, деревья являлись явно более сложным решением, и за разумное время их пользу для своей задачи я осознать не смог. Так что, видимо, в другой раз.
С конечным автоматом, впрочем, тоже не всё гладко. Вот, скажем, у нас есть состояние “бот передвигается из точки A в точку B”, а есть состояние “бот атакует цель”. Что если я хочу, чтобы эти два состояния могли быть у бота как по отдельности, так и вместе?
Тут на помощь приходят иерархические конечные автоматы (HSFM). Которые я неправильно понял, и вместо них изобрёл велосипед. Подозревал это я уже давно, но убедился только сейчас. В любом случае, вот все состояния, которые могут быть у NPC:
Передвижение: покой, погоня за целью, бой (имеются в виду те самые перемещения и поиск укрытия, чтобы не быть простой целью. В коде я это назвал
TacticalState
).Бой: покой, атака, перезарядка.
То есть здесь чётко видно две параллельные плоскости состояний NPC, и поэтому я, недолго думая, (по сути забыв про HFSM) разделил состояния на два подтипа. Вот итоговая структура состояний, которая у меня получилась:
Ещё я добавил интерфейс условий (ICondition
). Логично, что если у NPC вдруг куда-то пропало оружие, то ему больше не надо пытаться стрелять и перезаряжаться. Чтобы не делать одни и те же проверки в каждом состоянии, перед выполнением состояния я и выполняю эти проверки. Вот картинка с условиями, которые по итогу попали в мозги NPC:
Кратко, итоговая схема выглядела так:
Раз в некоторый отрезок времени срабатывает таймер, запускающий обработку состояний;
Сначала проверяются условия. При необходимости, текущее состояние меняется;
После этого наконец вызывается логика в состояниях. У них она находится в методе
Think
.
Детали реализации
Вдаваться в то, как реализовывать конечные автоматы и паттерн состояние я не буду, вместо этого покажу, как это работает у меня. Общая логика такая:
Состояния покоя в обоих случаях лишь ждут, когда появится возможность переключить состояние на другое. Для этого цель должна появиться и находиться в нужном радиусе.
Стрельба и перезарядка совсем скучные: если есть патроны, а также если мы вообще можем попасть в цель, то
IsShooting
боту ставим вtrue
. Иначе перезаряжаемся. Повторять до бесконечности.В случае погони мы лишь устанавливаем (и периодически обновляем) координаты, к которым нашему боту стоит бежать. Как туда добраться - бот сам спросит у NavMesh. После этого ждём, когда уже можно будет перейти в боевой режим (
TacticalState
).TacticalState
здесь самый интересный. ИИ ищет вокруг себя укрытие и перемещается за него, иначе просто хаотично передвигается. Ниже блок-схема алгоритма ("tick" это вызовThink
):
И, кажется, на этом с самым сложным разделом всё. После реализации алгоритма у меня уже был функциональный бот, похожий на своих коллег из HL2. Но перед тем, как продолжить улучшать его, я захотел сделать демонстрацию уже существующих возможностей.
Игра за один день
Чуть больше чем за неделю до конца приёма заявок меня посетила отличная идея: надо сделать режим, в котором я смогу тестировать ИИ, а также его можно будет показывать другим.
Общая концепция мне пришла довольно быстро: арена, где надо отбивать конечное число волн врагов. Между волнами ты можешь прокачиваться сам, а вместе с тобой усиливаться будут и враги. И сделать я его планировал аж за один день. Мне ведь надо было всего лишь:
Сверстать три менюшки (распределение очков персонажа, выбор оружия, конец игры);
Сделать спавнер врагов;
Сделать сущность, которая всё это свяжет и будет “скелетом” игры (по итогу им оказался `RoundManager`).
Ну и короче говоря: за день я всё не сделал.
Шутка за шуткой
Если оценивать что прежде всего пошло не так, то это, конечно, плохое планирование. Если сложить всё потраченное время на арену, то выйдет где-то 2-3 рабочих дня. Ещё сильно повлияло и то, что праздники кончились, повлияв на настрой.
Но недоработки в платформе тоже внесли свои коррективы. Из того, что пошло не так:
При создании проекта для s&box надо ввести текстовый идентификатор, который потом используется для поиска по сайту, а внутри влияет на имя проекта в решении. Проблемы были (я точно забыл какие) когда я вводил туда буквы в верхнем регистре. Но вот когда я после этого ввёл в идентификатор точку - у меня со всех объектов слетели компоненты и пропали насовсем. Где-то час я методом тыка доходил до того, что дело было в этой точке.
В какой-то момент я решил добавить инструмент в редактор, и несмотря на то что я чётко шёл по документации ничего просто не происходило, Спустя полчаса перезапусков редактора инструмент внезапно появился.
На последней неделе перед сдачей внезапно сломался NavMesh. Я несколько дней не мог понять что я сломал, пока наконец не вычитал в дискорде, что я такой не один, и оттуда же позаимствовал исправление (NavMesh надо было всегда перегенерировать).
Когда я в последние дни рефакторил проект и искал ошибки, то внезапно выяснил, что NRE падал из кода не по моей вине, а при вызове
NavMesh.GetSimplePath
иGetComponent
. При этом исключение из первого метода ломает режим, так как пути у ботов больше не находятся.С какого-то момента у меня просто начал появляться лишний префаб игрока на сцене каждый раз, когда я запускаю редактор.
В последний день у меня внезапно стали дублироваться соединения, что приводило к таким забавным багам:
В общем, приходилось идти вопреки многим проблемам, возникавшим почти каждый день. Бывали и более прозаические ситуации: как когда я отлаживал нетворкинг я не понимал, почему свойство `IsProxy` даёт некорректный результат. Кто же знал, что его надо звать после `OnStart`, а не после `OnAwake`, если в документации к методам об этом ни слова (напоминаю, в геймдеве я любитель).
Смена концепции
Где-то тут я понял, что пора менять идею, ведь у меня из готового только интерфейсы. А подаваться на джем с библиотекой, где из переносимого есть только это:
public interface ISbokuState
{
public void Think();
public void OnSet();
public void OnUnset();
}
Было, ну, сомнительно. В общем, стало понятно, что сделать тот самый "умный" ИИ я скорее всего просто не успею, поэтому было решено сфокусироваться на двух вещах:
Доделать арену.
Сделать уже существующую логику переносимой на любую оружейную базу.
Второй пункт был нужен, так как при разработке я полностью завязывался на SWB и как-то даже не думал, что это может выйти боком (надеюсь, вы оценили каламбур).
Для этого надо было выделить SbokuBase
в абстрактный класс и отвязать от SWB, а от него наследовать BotAdapter
. Во время разработки это были два не связанных компонента, одинаково прибитых гвоздями к оружейному паку (первый был моей реализацией NavMesh агента, а второй SWB контроллера). ещё и состояния на SWB завязывались. Так я и решил остановиться на варианте "минимум".
Ср(а/о)ки горят
Так и наступили последние три дня до закрытия заявок, а у меня ещё не была готова арена, которую я хотел сделать за день. В режиме аврала мне предстояло сделать три вещи.
Арена
Тут сказать особенно нечего, надо было просто сесть, сделать, протестировать и повторить.
Из интересного отмечу: мне сильно повезло, что UI в s&box делается на razor разметке, перекочевавшей из Blazor. Ещё и scss из коробки. Вот уж не думал, что мой опыт из веба будет здесь полезен.
Карта
Следующей на очереди была карта, ведь во время тестирования я пользовался этой вот этим, слепленным из кубов.
Но надо было сделать что-то поприличнее. И тут мне тоже повезло: в своё время я делал карты в Hammer Editor для CSS. Жаль только, что Source 2 использует новую версию Hammer Editor, которая от старой отличается примерно всем.
Новый редактор хорош, но есть у него один недостаток - руководств ну очень мало. Однако мне это не помешало создать впечатляющую коробку с небольшой вертикальностью.
Выглядит неказисто, но если накинуть немного объектов и тумана, то получится даже почти прилично.
Библиотека
Наконец разделавшись с игрой, мне оставалось сделать то, ради чего всё это и затевалось - свою библиотеку, которую можно переиспользовать в других проектах.
В целом, мою цель я уже описал в разделе “Смена концепции”. И это оказалось несколько проще, чем я думал. Так, в расслабленных доработках и рефакторинге для меня и прошёл последний день.
Но ещё одну интересную ремарку я здесь сделаю. В s&box нельзя использовать nuget. Вместо этого там своя система библиотек, которые загружаются прямиком на сайт. Так должно быть безопаснее (в s&box можно использовать только пространства имён из белого списка), а ещё они хранятся прямо у тебя в решении и их можно редактировать самому, если захочется. Библиотеки, правда, тоже ещё не доработаны - они не могут использовать друг друга.
Релиз
А что в конце? А в конце я успешно подал заявку в последний день вместе с ещё 99-ю участниками. Надо сказать, крутых заявок довольно много, в том числе по смежному профилю со мной. Как бы то ни было, результаты будут известны уже в начале следующей недели.
Сам игровой режим опубликован здесь, библиотека тут, а делят они общий репозиторий на GitHub. Да, всё находится в Open Source, так что если кажется, что что-то можно доработать - то добро пожаловать. Как по мне, пространства для улучшения и правда ещё много.
Здесь же выражу благодарность авторам SWB - без вас этого режима бы не было.
Послесловие
Несмотря на то, что всё ощущалось как не самая удачная попытка взять “с наскока” сложную тему, участие мне показался интересным. Всё-таки я и правда узнал много нового.
Да и находится в ряду с другими крутыми работами приятно. Больше всего меня впечатлил интерпретатор Lua на C# (профессиональная деформация), сделанный с помощью портирования оригинального интерпретатора с на C#. Почитайте статьи автора, там интересно.
Если оценивать разработку на платформе s&box в целом, то тут уже сложнее. В ней чувствуется потенциал, и после того же Unity тут дышится намного свежее, но пока экосистема сыровата. Уж слишком много палок в колёса прилетает в процессе, а из-за проблем с обратной совместимостью иногда приходится чинить то, что нормально работало до обновлений. Поэтому я со спокойной душой пока приостановлю разработку своего прошлого проекта и сфокусируюсь на доработках Sboku Arena.
И на этом, пожалуй, пора прощаться. Если вам интересен мой блог, то следить за мной можно в X и LinkedIn.
Комментарии (4)
gybson_63
26.01.2025 12:01Мне кажется s_and_boku bot не совсем удачный каламбур для s_and_box
Volokhovskii Автор
26.01.2025 12:01Шутка ли, я как-то и не думал, что & в названии надо читать вслух. Спасибо, что просветили :)
Jijiki
классно, я по картинкам понял что у вас получилось, заметил у вас превалируют правильно геометрически пропорциональные обьекты(то что декорации), планируете пещеры/горы/впадины/террейны? вот например у вас на рукаве горы, а вокруг геометрия прям геометрия )
сурс 2 интересно, вчера читал несколько статей, и сейчас на вашем обзоре проникся кривыми декорациями(с кривизной с шумом)
Volokhovskii Автор
Всё-таки целью создания игрового режима было испытание ИИ и какой никакой геймплей, поэтому художественная ценность для меня была не на первом и не на втором месте. Да и не вижу ценности давить из себя то, в чем мало понимаю.
Но было бы неплохо сделать что-то не состоящее из dev ассетов полностью (как сейчас). Может, даже до этого дойду, но пока так.