Disclaimer. Не смотря на то, что некоторая работа уже проделана, в статье не предлагается окончательное решение, а скорее описывается идея и подход создания такой открытой библиотеки и делается привлечение внимание к описываемым проблемам при создании игр. Автор, не смотря на то, что является профессиональным программистом, область создания игр для него является не более чем просто хобби.

Продумывая программную архитектуру различных прототипов игр на Unity 3D, решил поделится рядом соображений и заодно структурировать, и описать свой подход к реализации архитектуры. Конечно, очень редко можно договорится о соблюдении некоторой архитектуры при разработке. Тут как правило две проблемы в описании архитектуры как законченной сущности, и её понимании другими.

Нам потребуется многослойное понимание проблемы, к сожалению, язык последовательно синхронный и не позволяет сразу дать всесторонние описание. Но начнем мы с более простого, с описания частной проблемы, концепции класса AgentPoint, для глубины понимания я буду давать ссылки, изучение которых позволит понять проблему детализировано. В статье же скорее останется лишь легковесное описание, не рассчитывайте на глубокое понимание, если не пройдете по ссылкам. Но тем не менее я попробую, основы объяснить прямо тут.


Описание архитектуры. + Описание класса.

Концепция класса AgentPoint достаточно проста – это абстракция "точка агента" - любая точка в пространстве, в которой или рядом с которой могут находиться/работать/и т.п. агенты. Она нам понадобится как пример игрового понятия.

Проблемы, баги и плохие решения в Unity 3d

Поймите правильно, Юнити развивается достаточно быстро и вряд ли есть лучший условно открытый игровой движок. Часто версии Юнити меняются так быстро, что прототипы, которые я пишу успевают устареть быстрее, чем я их заканчиваю. Но у Юнити очень много проблем и неправильных решений, наверно есть исторические причины почему эти решения были приняты, и многие захотят наверняка мне это указать в комментариях. Но мне как разработчику логики игр – их проблемы не важны. Но мне приходится останавливаться и решать их “недоделки”, пытаясь потом созданные костыли спрятать за архитектурными абстракциями. Для демонстрации, покажу проблему, с которой недавно столкнулся.

Допустим у нас есть карта города, пусть он называется Ротарк. Всё максимально просто есть улицы и дома, те самые объекты класса AgentPoint. Размеры города соответствуют небольшому реальному городу 8х8 кв. км. и порядка 4 000 строений с 10 000 жителями. Но для простоты рассмотрим только 10 жителей. И мы хотим совершенно простую вещь, чтобы они каждый день просыпались бы в 7.00 утра, шли бы на работу, а после работы в 18.00 шли бы домой спать. Всё. Казалось бы, ничего не предвещает проблем.

Если бы не чертовый Unity 3D и их кривые решения. Обращали ли вы внимание, что метод NavMesh.CalculatePath() может вернуть вам частично рассчитанный путь?

Давайте обратимся к документации. Там есть предупреждение:

"Эта функция синхронная. Она выполняет поиск пути немедленно, что может негативно повлиять на частоту кадров при обработке очень длинных путей. Рекомендуется выполнять лишь несколько поисков пути за кадр, например, при оценке расстояний до точек." и лишь косвенное упоминание "Возвращает: логическое значение True, если найден полный или частичный путь, и false в противном случае.".

И когда, ваш агент не доходит до своего дома/места работы и тупо останавливается где то на карте совсем не там, уверен вы думаете причина в вашем коде, и идете проверять правильно ли вы указали куда ему нужно идти. И находитесь несколько часов в прострации еще догадываясь, что путь оказывается посчитался частично. Что это значит вообще частично?

Вместо того, чтобы в описании CalculatePath() просто указать, когда путь будет посчитан частично, а когда полностью, там какое то неясное предостережение. И дальше нужно применить всю мощь ИИ LLM, чтобы найти соответствующие объяснения на форумах и понять, что он тупо считает в течении одного фрейма и сколько успевает посчитать столько считает, а потом ставит «посчитал частично и отвали». И вот уже вышла шестая версия Юнити, а решения как не было, так и нет.

Чему нас учит этот пример? И как появилась TacLibrary?

Юнити предоставляет достаточно низко уровневые понятия и логику построения игрового пространства. Нет ни каких высокоуровневых концепций, даже такая простая концепция как абстракция "точки агента" в которую агенты хотят попасть по расписанию или по какой-либо другой расширяемой логике – работать из коробки не будут. Более того, чтобы продумать архитектуру вы это сделаете в контексте разработки конкретной вашей игры. И столкнувшись с проблемой в 99% сделаете костыль, который возможно и будет решать проблему, но никто кроме вас его повторно использовать не сможет.

И таких «костыльных решений» у вас со временем накапливается достаточное количество. Но они все привязаны к тому или иному прототипу/игре и наверняка не будут работать в другой связке.

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

Вот такой опыт и заставил меня выделить некую DLL с пакетом тестовых моделей, назовем её TacLibrary, в которой будет реализована более высокоуровневые концепции игровых понятий.

Самое тяжелое это писать документацию, и я только в начале этого пути - тут то, что я успел описать.

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

Решение описанной выше проблемы Юнити в репозитории реализовано, как и многое друг��е, но это тема отдельного туториала.

Архитектурная легкость и partial-подход

Не каждое функциональное решение разработчик хочет тянуть «к себе в проект». И этому есть куча причин. И главное — это возможность легкой модификации и адаптации под свои нужды. Часто оказывается, что легче реализовать “под себя, под свой проект” самому, что называется с нуля. Тут есть две крайности “движкописательство”, пытаясь сделать с нуля, тоже самое что делает Юнити, но по-своему. И вторая крайность: “настройка ассетов”. На практике, использование крупных ассетов приводит лишь к бардаку в коде, никто не в состоянии разобраться в тысячи параметрах и аспектах и прямо править/настраивать в коде или даже в редакторе Юнити без подробной документации, а как правило поддержка ассетов не ведется, и они устаревают быстрее, чем вы закончите свою игру, как правило после ново версии Юнити. Уж что-что, а переписывать код чужого ассета, который вы используете только на 5-10%, чтобы после обновления версии Юнити он оставался работоспособным, вы не будете. Да, и вообще, тянуть в свой проект тонну непроверенного кода это стремное решение.

Поэтому на первый план выходят очень короткие, но стабильные приемы и решения. И скорее просто проработка концепций объектно-ориентированной декомпозиции игровых понятий, чем функциональный код как таковой. И чем больше, архитектурной простоты тем лучше. Все эти оверхед подходы вида ECS, различные виды MVC, перегрузка кода интерфейсами, скриптайбел объектами и прочими новомодными концепциями, и веяниями с каким-то никому не нужным мудрствованием от лукавого – лишняя трата времени.

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

Тут кратко описан т.н. partial-подход.

А здесь я объясняю об этом подробнее

По сути, речь идет о не совсем классическом использовании ключевого слова partial class. И дискуссия часто сводится к тому, что физически все компилируется в единый класс и якобы это тоже самое, что мы ушли в 80-е, и программируем без классов, т.к. это все равно что объявлять в одном большом глобальном классе/куче. Конечно, такая критика не состоятельна, в репозитории хорошо видно, когда и где применяется partial, и это не создает ни каких огромных классов. Скорее наоборот, этот подход разделяет роли объекта на части.

Но это совсем другой разговор, о том, что слепое применение принципа единой ответственности (SRP) лишь делает хуже:

Принцип единой ответственности (SRP) - что с ним не так? Начало.

Принцип единой ответственности (SRP) - что с ним не так? Часть №2.

Принцип единой ответственности (SRP) - что с ним не так? Часть №3.

Рассматриваем книгу Б. Мартина "Быстрая разработка программ", конкретно пытаясь понять как им и Б. Косом программировалась игра "Боулинг" (этому посвящена одна из глав). Делаем это, чтобы понять вначале контекст как возник и кто тот человек, который предложил принцип единой ответственности. Понимаем, что это "заядлый структурщик", который пробывал себя в роли ООП`шника, подробное рассмотрение "игры в боулинг" однозначно приводит нас к этому пониманию. Подтверждается, что этот принцип (единой ответственности) позволяет немного сгладить эффекты, когда на объектно-ориентированном языке программируют те, кто думают исключительно структурно. Получается немного лучше, чем полностью в структурной (процедурной) парадигме, но все же далеко, если думать сразу в терминах ООП. Показывается пример того, если программировать "игру боулинг" изначально с помощью ООП, и становится понятно чем это отличается.