У меня за плечами несколько лет работы в 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), но, к счастью, эту идею я быстро отмёл из-за сложности.

Иронично, но по итогу на джем была подана заявка, в которой был реализован местный Space Engine.
Иронично, но по итогу на джем была подана заявка, в которой был реализован местный Space Engine.

Так что я принялся думать, что я вообще смогу осилить. Так как я являюсь любителем одной известной серии игр про выживание, аномалии и охоту за артефактами, мне пришла простая идея: сделать ИИ для ботов, которые смогли бы тебя окружать. В игре это обычно сопровождается запоминающимся криком “Сбоку, сбоку заходи!”. 

Так и родилось название для моего проекта - SbokuBot. Являлось ли ошибкой, что я не назвал его s&boku bot, воспользовавшись очевидным каламбуром, я думаю до сих пор.

Мне казалось, что это удачный выбор, ведь игровой ИИ часто строится на основе графов, а это уже ближе к моим профессиональным навыкам (совсем недавно писал об этом статью). Первоначальная идея звучала как-то так: 

Сделать ботов, которые будут тебя окружать, активно используя при этом укрытия. Они будут прятаться за ними пока перезаряжаются, а перемещаться между ними перебежками, пока их союзники отвлекают на себя внимание.

Тогда же я начал прикидывать, как это вообще можно реализовать. В s&box уже есть NavMesh с API, который позволяет строить по нему пути. Самым сложным тогда казался вопрос укрытий. Как их определять? Вручную? Сканировать местность и запоминать? Выводить из геометрии?

Сначала я хотел сделать это при помощи весового NavMesh, но впервые открыв API reference, я был удивлён его минималистичностью. Максимум, что с ним можно сделать - это получить путь из точки A в точку B. Из-за этого как-то уточнить идею не вышло, так что пришлось действовать исходя из довольно общего описания задачи.

Визуализация NavMesh в s&box
Визуализация NavMesh в s&box

Начало разработки

Далее встал вопрос, с чего вообще начать. Так как в своей основе s&box - это почти тот же самый Unity, где всё, включая контроллер персонажа, надо делать с нуля, базироваться на чём-то было скорее необходимостью.

Сами Facepunch сделали свой шутер Nicked, который я и хотел изначально форкнуть. Благо, у него открытый исходный код.

Counter-Strike подобный режим
Counter-Strike подобный режим

Но немного почитав обсуждения, я отказался от этой мысли. Всё-таки он слишком тяжеловесный, и лучше было бы использовать что-то более простое. Так я и вышел на 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 - они никогда не казались мне особенно умными, но и как столбы никогда не стоят - либо просто делают случайные перемещения, либо прячутся за укрытиями. Так что это стало по совместительству и моей первой ступенькой, и вариантом “минимум”.

Конечный автомат

Немного рассуждений о теории

Где-то через неделю после начала работы я, наконец, взялся за что-то, напрямую связанное с ИИ. И здесь же плотно встал вопрос: каким образом это реализовать?

Быстрый поиск показал три варианта:

  1. Конечный автомат, где бот переключается между разными состояниями (меняет свой алгоритм действий целиком).

  2. Поведенческое дерево.

  3. Дерево решений.

И я не просто так не дал никакой ремарки касательно второго и третьего варианта. Я их не понял :). В то время, как конечный автомат выглядит как частный случай известного мне паттерна состояние, деревья являлись явно более сложным решением, и за разумное время их пользу для своей задачи я осознать не смог. Так что, видимо, в другой раз.

С конечным автоматом, впрочем, тоже не всё гладко. Вот, скажем, у нас есть состояние “бот передвигается из точки A в точку B”, а есть состояние “бот атакует цель”. Что если я хочу, чтобы эти два состояния могли быть у бота как по отдельности, так и вместе?

Тут на помощь приходят иерархические конечные автоматы (HSFM). Которые я неправильно понял, и вместо них изобрёл велосипед. Подозревал это я уже давно, но убедился только сейчас. В любом случае, вот все состояния, которые могут быть у NPC:

  • Передвижение: покой, погоня за целью, бой (имеются в виду те самые перемещения и поиск укрытия, чтобы не быть простой целью. В коде я это назвал TacticalState).

  • Бой: покой, атака, перезарядка.

То есть здесь чётко видно две параллельные плоскости состояний NPC, и поэтому я, недолго думая, (по сути забыв про HFSM) разделил состояния на два подтипа. Вот итоговая структура состояний, которая у меня получилась:

Визуализация иерархии классов с небольшим упрощением
Визуализация иерархии классов с небольшим упрощением

Ещё я добавил интерфейс условий (ICondition). Логично, что если у NPC вдруг куда-то пропало оружие, то ему больше не надо пытаться стрелять и перезаряжаться. Чтобы не делать одни и те же проверки в каждом состоянии, перед выполнением состояния я и выполняю эти проверки. Вот картинка с условиями, которые по итогу попали в мозги NPC:

Их немного, но это лучше, чем копировать код.
Их немного, но это лучше, чем копировать код.

Кратко, итоговая схема выглядела так:

  1. Раз в некоторый отрезок времени срабатывает таймер, запускающий обработку состояний;

  2. Сначала проверяются условия. При необходимости, текущее состояние меняется;

  3. После этого наконец вызывается логика в состояниях. У них она находится в методе 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();

}

Было, ну, сомнительно. В общем, стало понятно, что сделать тот самый "умный" ИИ я скорее всего просто не успею, поэтому было решено сфокусироваться на двух вещах:

  1. Доделать арену.

  2. Сделать уже существующую логику переносимой на любую оружейную базу.

Второй пункт был нужен, так как при разработке я полностью завязывался на SWB и как-то даже не думал, что это может выйти боком (надеюсь, вы оценили каламбур).

Для этого надо было выделить SbokuBase в абстрактный класс и отвязать от SWB, а от него наследовать BotAdapter. Во время разработки это были два не связанных компонента, одинаково прибитых гвоздями к оружейному паку (первый был моей реализацией NavMesh агента, а второй SWB контроллера). ещё и состояния на SWB завязывались. Так я и решил остановиться на варианте "минимум".

Ср(а/о)ки горят

Так и наступили последние три дня до закрытия заявок, а у меня ещё не была готова арена, которую я хотел сделать за день. В режиме аврала мне предстояло сделать три вещи.

Арена

Тут сказать особенно нечего, надо было просто сесть, сделать, протестировать и повторить. 

Из интересного отмечу: мне сильно повезло, что UI в s&box делается на razor разметке, перекочевавшей из Blazor. Ещё и scss из коробки. Вот уж не думал, что мой опыт из веба будет здесь полезен.

Рабочее окружение как в старые добрые
Рабочее окружение как в старые добрые

Карта

Следующей на очереди была карта, ведь во время тестирования я пользовался этой вот этим, слепленным из кубов.

Просто и сердито
Просто и сердито

Но надо было сделать что-то поприличнее. И тут мне тоже повезло: в своё время я делал карты в Hammer Editor для CSS. Жаль только, что Source 2 использует новую версию Hammer Editor, которая от старой отличается примерно всем.

Шутки в сторону, редактор сильно прокачался. Теперь он больше походит на Blender.
Шутки в сторону, редактор сильно прокачался. Теперь он больше походит на Blender.

Новый редактор хорош, но есть у него один недостаток - руководств ну очень мало. Однако мне это не помешало создать впечатляющую коробку с небольшой вертикальностью.

Наконец-то моих навыков на что-то хватило.
Наконец-то моих навыков на что-то хватило.

Выглядит неказисто, но если накинуть немного объектов и тумана, то получится даже почти прилично.

Результатом я доволен. Куда расти, конечно, есть.
Результатом я доволен. Куда расти, конечно, есть.

Библиотека

Наконец разделавшись с игрой, мне оставалось сделать то, ради чего всё это и затевалось - свою библиотеку, которую можно переиспользовать в других проектах.

В целом, мою цель я уже описал в разделе “Смена концепции”. И это оказалось несколько проще, чем я думал. Так, в расслабленных доработках и рефакторинге для меня и прошёл последний день.

Но ещё одну интересную ремарку я здесь сделаю. В s&box нельзя использовать nuget. Вместо этого там своя система библиотек, которые загружаются прямиком на сайт. Так должно быть безопаснее (в s&box можно использовать только пространства имён из белого списка), а ещё они хранятся прямо у тебя в решении и их можно редактировать самому, если захочется. Библиотеки, правда, тоже ещё не доработаны - они не могут использовать друг друга.

Релиз

А что в конце? А в конце я успешно подал заявку в последний день вместе с ещё 99-ю участниками. Надо сказать, крутых заявок довольно много, в том числе по смежному профилю со мной. Как бы то ни было, результаты будут известны уже в начале следующей недели.

Оно работает
Оно работает

Сам игровой режим опубликован здесь, библиотека тут, а делят они общий репозиторий на GitHub. Да, всё находится в Open Source, так что если кажется, что что-то можно доработать - то добро пожаловать. Как по мне, пространства для улучшения и правда ещё много.

Здесь же выражу благодарность авторам SWB - без вас этого режима бы не было.

Послесловие

Несмотря на то, что всё ощущалось как не самая удачная попытка взять “с наскока” сложную тему, участие мне показался интересным. Всё-таки я и правда узнал много нового.

Да и находится в ряду с другими крутыми работами приятно. Больше всего меня впечатлил интерпретатор Lua на C# (профессиональная деформация), сделанный с помощью портирования оригинального интерпретатора с на C#. Почитайте статьи автора, там интересно.

Если оценивать разработку на платформе s&box в целом, то тут уже сложнее. В ней чувствуется потенциал, и после того же Unity тут дышится намного свежее, но пока экосистема сыровата. Уж слишком много палок в колёса прилетает в процессе, а из-за проблем с обратной совместимостью иногда приходится чинить то, что нормально работало до обновлений. Поэтому я со спокойной душой пока приостановлю разработку своего прошлого проекта и сфокусируюсь на доработках Sboku Arena.

И на этом, пожалуй, пора прощаться. Если вам интересен мой блог, то следить за мной можно в X и LinkedIn.

Комментарии (4)


  1. Jijiki
    26.01.2025 12:01

    классно, я по картинкам понял что у вас получилось, заметил у вас превалируют правильно геометрически пропорциональные обьекты(то что декорации), планируете пещеры/горы/впадины/террейны? вот например у вас на рукаве горы, а вокруг геометрия прям геометрия )

    сурс 2 интересно, вчера читал несколько статей, и сейчас на вашем обзоре проникся кривыми декорациями(с кривизной с шумом)


    1. Volokhovskii Автор
      26.01.2025 12:01

      Всё-таки целью создания игрового режима было испытание ИИ и какой никакой геймплей, поэтому художественная ценность для меня была не на первом и не на втором месте. Да и не вижу ценности давить из себя то, в чем мало понимаю.
      Но было бы неплохо сделать что-то не состоящее из dev ассетов полностью (как сейчас). Может, даже до этого дойду, но пока так.


  1. gybson_63
    26.01.2025 12:01

    Мне кажется s_and_boku bot не совсем удачный каламбур для s_and_box


    1. Volokhovskii Автор
      26.01.2025 12:01

      Шутка ли, я как-то и не думал, что & в названии надо читать вслух. Спасибо, что просветили :)