Posted December 14, 2025 by tyapichu
Предыдущий пост по проекту я написал еще в Июле (прикольная привычка писать названия месяцев с большой буквы образовалась, скоро начну пить чай вместо воды). С тех пор чо-то изменилось.
Я очень пытался избежать проблем с тем чтобы каким-то особым образом создавать инстансы данных для боевки. В результате у меня на всю змейку получался один инстанс персонажа… соответственно только один персонаж мог атаковать или получать урон. В общем это не работало.
Пришлось заморочиться и добавить в игру еще один слой данных: собственно инстансы. Теперь у меня есть целый торт наполеон состоящий из конфига, рантайма (сохраненки), контроллера (меты) и инстанса (боевки). Инстанс использует контроллер так же как контроллер использует конфиг - источник базовой информации. Свойства и раньше имели пометку мета/кор, но теперь эта пометка говорит не о создании или несоздании инстанса, а о том будут ли данные браться из конфига (если свойство действует только в боевке - текущее здоровье персонажа) или из меты (если свойство действует и в боевке, и в мете - максимальное здоровье персонажа). При этом инстанс создается в любом случае потому что обращения работают только внутри слоя: мета к мете, инстанс к инстансу. Иначе совсем неконтролируемо получается.
Проблема была в том, что мне очень не хотелось лезть в дата-фабрику и что-то в ней достраивать - уж слишком геморно это выглядело даже в мыслях, не говоря уже о реализации. В действительности получилось даже хуже чем в моих самых страшных фантазиях - новая сущность добавляла десяток дополнительных строк, а новый слой просто умножает в два раза объем описания существующих сущностей. Очень тоскливо.
В результате я таки переписал основную фабрику игры, разделив ее на две: для Карточек и для Опций - и избавившись от всей возможной универсальности, которая была когда-то по незнанию в нее заложена. Две отдельные фабрики в сумме стали занимать в 2 раза меньше строк чем одна большая. Это сделало один из генериков, которыми я так гордился, бесполезным. Пришлось его удалить.
Кстати открыл для себя прикольный алгоритм фабрики, в котором нужные функции не перечисляются свичем или ифами, а сложены в словарь. Этот словарь конечно надо еще наполнить, что вызвало у меня некоторые сложности, но это прям на пару порядков проще чем было раньше. Я в восторге.
Следующий слой проблемы заключался в том, что у меня в игре есть довольно хорошая система вьюх, которая была заточена под определенную архитектуру классов и не предполагает никаких надстроек. Вьюха может привязаться к контероллеру карточки или свойству, но ничего не знает про инстансы. Но наследовать инстансы от контроллеров либо просто не работает, либо чревато ошибками (да я уже тупо забыл почему я в интерфейсы пошел). И я бахнул интерфейсы. И мне пришлось под них переделать вообще все: и фабрики, и вьюхи. Благо это “все” на самом деле не так уж и много и достаточно просто и надежно чтобы переделка не привела к катастрофе. Но в этом месте структура проекта начала походить на салат оливье. Я вообще не горжусь этим. Но работает.
Вообще, незнание довольно сильно ударило по моим планам и самомнению. Одна из корневых проблем заключалась в том, что я таки не знал как построить ролевую систему игры и какую ответственность должны нести те или иные ее компоненты. Да, здравствуйте, меня зовут Гриша, я 19 лет в геймдеве и впервые с нуля проектирую ролевую систему игры, в которой нет удочек, наживок и прикормок.
Действительно, изначально у меня была какая-то мешанина из свойств. У меня все было всем одновременно. Сначала я разделил эту мешанину на Карточки и Опции. Опции меня пугали, я не знал сколько их будет и чем они будут заниматься, у меня вызывало тоску необходимость заводить новые опции, я боялся что их будет бесконечное множество.
Но со временем оказалось что никакой универсальности на самом деле нет. Есть вполне конкретные виды опций, которые занимаются вполне конкретными задачами, с помощью каких-то систем (это будет важно потом).
На самом деле можно было бы даже вариант с прибитием гвоздями убрать, и просто использовать Статы, но заводить Стат просто чтобы дать свойству атрибута какое-то значение “поумолчанию” кажется слишком жирно. А учитывая еще что у меня нет прямых ссылок между объектами, все делается через Теги, то будет очень легко запутаться.
Статы были самыми пугающими для меня типами опций, потому что я реально не знал сколько их может быть. Но в результате оказалось что их может быть всего 2.5: те что я описал выше + значение прибитое к полу. У меня было два отдельных класса статов под эти случаи, но я их соптимизировал до одного с простым указателем в какую сторону воевать.
Разница между моментальным и временным эффектом действия Изменятора и Модификатора делают их кардинально разными системами.
Интересно что Изменятор можно использовать не только для собственно боевой ролевки нанесения урона, но и например чтобы списывать энергию игрока при запуске уровня. Фишка тут в том что он кроме действия содержит какое-то значение… которое может быть связано с каким-то Статом. Например менять стоимость входа в бой в зависимости от уровня игрока. Все это конечно можно было бы просто продублировать в свойствах класса, но зачем если есть просто это готовый механизм. Можно и не через ссылку на отдельный эффект, а встроить в класс как поле. Не суть.
У Статуса и Модификатора намного больше общего чем с другими эффектами. Их можно использовать на нескольких “уровнях”: на всех объектах боя или игры вцелом, на конкретного персонажа или на всю змейку. Из-за этого для работы статусов и модификаторов используются специальные контроллеры на уровне проекта которые не зависят от того, на кого эффекты накладываются. Это особенно важно для модификаторов, так как они по сути выполняют роль передачи “бонусов” от одних игровых объектов другим.
Это в свою очередь позволяет делать некоторые фокусы. Например, сделать механизм ауры, который будет усиливать персонажей змейки игрока если в ее составе будет сегмент с этим свойством. Больше я пока не придумал.
Отдельный восторг у меня вызывает то что порядковый номер комнаты на уровне - это Атрибут. Его значение я буду использовать в Стате, который преобразует номер комнаты в коэффициент сложности. Модификатор же будет раздавать это значение всем, кому нужно.
Взаимозависимость Опций друг от друга имеет одного скрытого героя. Это тот самый параметр который может хранить какое-то прибитое к полу значение, либо ссылку на Стат. Одна из больших моих проблем заключалась в том, что я в том числе не знал а только ли на Стат или быть может на какие-то еще виды Опций. А главное, как получать значения от других объектов, которые теперь реализуются (будут реализованы) через Модификаторы. Однозначность ответственности - моя любимая штука теперь.
Единственная оставшаяся проблема заключалась в порядке инициализации. Меня очень пугала идея инициализировать Опции по запросу. Но оказалось все довольно просто. Я даже умудрился реализовать “защиту” от цикличных зависимостей, которая просто роняет игру при их обнаружении.
Я довольно сильно страдал от стейт-машины, которую перетащил в новый проект из предыдущей попытки. Стейт-машина там была красивая и мощная, ну как генерики в фабрике. Но кажется выполняла совсем не ту роль которую должна была бы. В результате я вернулся к простой компонентной системе в которой за текущее состояние отвечает корневой контроллер Сегмента. Он даже не знает про то какие компоненты в принципе у Сегмента существуют - ему пофиг, его дело раздавать состояние, тики и люлей. Получилось очень чистенько (спасибо Zenject), хоть и не решило всех моих проблем.
Одной из проблем старой стейт-машины было то, что она была синхронная. Это приводило к тому что некоторые процессы пытались происходить одновременно хотя могли противоречить друг другу. Два самых тонких места - это изменение состояние в процессе работы какого-то алгоритма и апдейты.
Чтобы размотать изменение состояния я использую очередь (буквально Quie), обновляющуюся покадрово. Теперь что бы ни произошло, какие бы алгоритмы ни срабатывали, но следующее состояние придет только в следующем кадре, когда текущие алгоритмы уже будут закрыты.
А вот с апдейтами пришлось повозиться. Потому что Сегменты имеют свойство на них подписываться и отписываться. С одной стороны чтобы не крутить лишние апдейты по выключенным Сегментам (совершенно излишняя на моем уровне забота о производительности), а с другой стороны, чтобы гарантировать что внутри сегмента ничего не будет случайно обновляться из-за забытой подписки. Это еще не считая того что подписки могут дублироваться. Причем в самых неожиданных местах.
И чтобы удостовериться в том, что подписка или отписка от апдейта произошли я пустился в страшное - асинхронщину. Эта хрень конечно работает, но создает не очень приятные каскады, в которых становится сложно понять что в каком порядке произойдет. Например, для работы змейки довольно важна связь между звеньями - ссылки на предыдущие и следующие звенья.
Звенья инициализируются просто в цикле, но в два захода: когда создается звено еще нет информации о следующем звене - оно еще не создано, зато можно предыдущему звену сказать что для него следующим является текущее… дааа.
Так вот цикл создания звеньев синхронный, а подписка на тики у звеньев асинхронные. В результате информация о следующем звене начала приходить в контроллер Сегмента раньше чем закончится его инициализация. С моей точки зрения это ахренеть как ненадежно. С другой стороны ЧатГПТ вполне успешно развязал эту хрень, таки позволив наконец собрать надежно работающий билд.
За какими-то мелкими исправлениями с одной стороны скрывается наконец появившийся стабильный билд, который не разрывает на куски при запуске или переходе на новый уровень. А с другой стороны я усиленно откладываю неизбежное: необходимость таки заняться визуальной стороной дела.
Причем проблема уже довольно серьезно стоит. Хотя бы потому что Статус отравления у меня уже есть, а отличить персонажа который просто урон наносит от персонажа который отравлением пуляется я не могу.