Comments 154
Используйте функциональный - в чем проблема?
Я пока не понял как вы подходом с таблицами данных предлагаете решить первоначальную проблему ооп с возастающей иерархичностью данных. Вот у вас один персонаж бъет другого мечём, при одетых перчатках с модификатором огня и должен соответсвенно учесть защиту аппонента и защиту от огня. Вам все равно придётся пройти по всем предметам персонажа и аппонента, каждый предмет обладает группой свойств - а значит иерархическая запись - таблица таблиц. Дальше вы выберете данные с учетом типа атаки и посчитаете результат по некторой формуле, внеся его в статы аппонента.
Кажется как будто тоже самое не сложнее написать с ооп - просто реализовав у атакующего метод - аыдающий сумаризированные данные атаки, а у защищающегося - принимающий их, считающий урон по внутренней формуле и применяющий.
Проблемы начинаются при реактивных штуках (если противник парировал атаку, даёт +100% к слеюущей) но они и в случае хранения данных в общей куче - точно такие же.
В своё время при разработке игры ооп очень круто помог масштабировать проект благодаря композиции. Игровые объекты были "актерами", и игровая сцена, содержащая в себе набор взаимодействующих актеров - тоже была актером (по интерфейсу). За счёт этого их можно было комбинировать как угодно, вкладывая одни готовые сценарии в другии в любых масштабах (как матрёшка, только круче).
Т.е. если актер это один юнит - который умеет только анимироватся и двигаться, то взвод имеющий некоторую логику построения - снаружи тоже актер, и армия - обладающая уже полноценным управлением и целями - тоже актер, и конечная сцена - может содержать несколько армий, несколько отдельных взводов, какое-то количество просто юнитов - и все это вместе с точки зрения движка - тоже 1 актер, которому просто передаётся управление.
Тут проблема состоит в том, что поведение реального солдата в одиночку и поведение его же в составе боевого порядка - совершенно разные вещи. Поэтому композиция даёт далёкие от идеала результаты. Условно говоря, сражение двух армий не декомпозируется до множества драк в подворотне.
Логика разная, но интерфейсы одинаковые. Каждый в армии может работать по командам армии, а не, например, игрока. Но при этом действовать как отдельная единица по тем же командам, которые даёт 1 игрок. То же со сражением.
Не понятно при чём тут композиция вообще. Новую логику так или иначе придётся отдельно реализовать, она само по себе не появится.
. Промахнулся
Можно это легко сделать шаблоном компановщик. Управлять N юнитами как одним.
Так в том-то и проблема, что N юнитами по уму надо управлять совершенно по-другому, чем одним.
По-другому это как? Юнит может иметь разные навыки в зависимости от того, в отряде он или нет? Он обладает всеми навыками сразу. А вот какие будут использоваться зависит от стратегии - одиночка, малый отряд, батальон.
Если используемые навыки юнита зависят не от него самого, а от способа его применения, то это не очень соответствует принципам ООП. Более того, зачем вообще нужны "навыки юнита" на таком уровне абстракции? Для результата сражения между армиями не важна индивидуальная подготовка конкретных солдат.
Возможно у вас видимо есть какой-то образ игры с юнитами отрядами, который может не совпадать с теми, кто вас читает. Опишите? Потому что для меня "юнит" умеет просто ходить и атаковать, а если он включается в отряд - просто мы управляем неким объектом, который отправляет команды ходить и атаковать юнитам.
Так и происходит в играх с простой механикой, диктуемой как раз принципом декомпозиции. Но в реальной жизни-то это не так. Управление одним солдатом заключается в стрельбе и рукопашном бое, командир батальона управляет тактикой солдат по рации, а генерал, командующий армией, диктует стратегические приказы стенографистке (и при этом минимум 2/3 армии выполняет небоевые задачи). В играх посложнее это пытаются реализовать, с той или иной степенью успешности. Типа, сначала императорский флот захватил планету, потом там по ней AT-AT прошлись танковым строем, а потом за отдельного солдата можно поиграть.
Про то и речь, что отряд в виде группы клонированных юнитов - это жуткое упрощение игровой механики.
Всё равно вас не понимаю, что вы имеете ввиду. Может приведёте пример? Классика RTS типа StarCraft, Warcraft, Homeworld, "Война и мир", Блицкриг и т.п. в таком "жутко упрощённом формате" не спроста стала классикой - видимо этого достаточно для создания увлекательного геймплея) А про какие игры думаете вы, когда описываете свою концепцию?
Не. Проблема состоит в том, что автор статьи сначала описывает проблему (высокую сложность модификации кода из за большой иерархичности данных при использовании ооп), а потом предлагает подход, который никак эту проблему как будто не решает.
Ну то есть даже безотносительно того, хороший ли подход к хранению всего игрового состояния в глобальной таблице или плохой - непонятно, как это влияет на то, что чтобы рассчитать урон меча с учётом перчаток и разных типов - все равно придется пройтись по таблице всех itemов, взять для каждого item подтаблицу параметров и по формуле где-то это свети с itemами оппонента, чтобы рассчитать урон.
Здесь это где?
Что-то не прописалась ссылка, спасибо. https://habr.com/ru/articles/919190/
Можно пример какой-то игры, написанной по таким принципам? Потому что сейчас кажется, что на бумаге всё хорошо, а на практике такой подход будет работать только для несложной адаптации настолки, где правила изначально были достаточно простые, чтобы их мог понять человек:
Выделите «скелет» сущности: активность, позиция, базовое здоровье — всё, что нужно почти всегда. Поместите в единый массив структур
Unit
.
Это будет работать, если взаимодействовать между собой будут только персонажи. Если же у вас можно нанести урон неодушевлённому объекту (фаербол ломает бочку) и неодушевлённый объект сам может быть источником урона (бочка взрывается), то у вас на сцене вместо десятка юнитов их будут сотни: каждое дерево, сундук и даже стул, на котором сидит персонаж - это теперь всё юниты. И их список постоянно меняется, изменяя наш массив: гоблин кинул в игрока гранату? Плюс юнит, потому что граната - это тоже юнит. И так у нас весь выигрыш в производительности растворяется о тысячи проверок каждый кадр "а нет ли у дерева мозгов и не надо ли обновить ему ИИ", потому что вместо массива компонентов Brain у нас указатели в Unit.
пора положить код на стол и спросить: «Какие данные и в каком порядке мне реально нужно обрабатывать в каждом кадре?»
Что если данных - миллион и порядок обработки не детерминирован? У вас, к примеру, диаблоид, где и на персонаже, и на противниках куча предметов с эффектами возврата урона, спавна проджектайлов на атаке и прочего вампиризма. И состав этих эффектов ещё и постоянно меняется в реальном времени. Тогда у вас одна функция расчёта урона растянется на тысячи строк. В таких случаях гораздо удобнее все эффекты атаки / защиты описать в виде классов и хранить их экземпляры в списках для каждого юнита. В кого-то прилетел урон? Проходим по спискам атакующего у цели чтобы посчитать его значение и запустить всю дополнительную логику.
Минимум динамических структур, максимум последовательного доступа к памяти.
Упарываться в оптимизацию игровой логики на уровне доступа к памяти в 2025 - ну такое себе. Сколько должно быть юнитов и как они должны себя вести чтобы это давало значимый прирост в сравнении хотя бы затратами на поиск пути для этих всех юнитов?
Упарываться в оптимизацию игровой логики на уровне доступа к памяти в 2025 - ну такое себе. Сколько должно быть юнитов и как они должны себя вести чтобы это давало значимый прирост в сравнении хотя бы затратами на поиск пути для этих всех юнитов?
Вон недавно анонсировали Ashes of the Singularity II с тысячами юнитов. Говорят, первая часть утилизировала все ядра процессора. Или всякие UEBS 2 с 1.3 миллионами юнитов. Движения юнитов я так понял там симулируется через физику жидкости(Fast Fluid Dynamics Simulation?), а не через какой-нибудь A*.
Кто «владеет» действием — меч, рука, аура или персонаж?
Дамаг наносит не рука, аура и не персонаж. Оружие. Всегда оружие, остальное — модификаторы, которые должно опросить оружие.
PS: если рука пустая, то оружие — «кулак», дробящее. Проще некуда.
Маг кастует фаербол. Почему урон должно наносить оружие, которое, в данном случае, на него вообще не влияет? Логичнее оперировать уроном как неким объектом, который могут породить разные источники: оружие, магия, ловушки и т. п. Этот объект хранит все необходимые модификаторы атакующего и при применении к цели опрашивает её модификаторы.
Логичнее оперировать уроном как неким объектом, который могут породить разные источники: оружие, магия, ловушки и т. п.
Не противоречит, просто это ещё более общее определение. Вполне принимается.
Маг кастует фаербол. Почему урон должно наносить оружие, которое, в данном случае, на него вообще не влияет?
Я бы сказал, что в данном случае оружие — сам файрболл.
Можно сказать и так, но тогда самому классу "оружие" будет сложно придать конкретное содержание. Между файрболом и мечом очень мало общего.
Как же там было… а, «оружие, покидающее руку».
Я, пожалуй, прокомментирую подробнее: тут получается либо а) каст файрболла (и любого заклинания) свободной рукой, что ничем не отличается от метательного ножа; б) каст с посоха/жезла, что ничем не отличается от арбалета; в) магия не с рук (вербальные заклинания, менталистика, амулеты) — реализованная магия становится также оружием, хотя с ограниченным сроком действия, т.е. создаётся, действует положенное время и уничтожается.
И да, это не меч, конечно же. И разные типы оружия ведут себя по-разному и имеют, соответственно, разный код. Но код должен сидеть именно в объекте оружия.
Но код должен сидеть именно в объекте оружия.
Почему же? Возьмем, к примеру, посох. Он не только файерболами умеет кидаться, но и много чего другого имеющие вполне себе уникальные характеристики (понижать здоровье, уменьшать скорость, накладывать дебафы и так далее).
Вполне логично, что удобнее посох описывать отдельно и отдельно все то, что из него вылетает. И описание того, что из него вылетает вполне удобно хранить и обсчитывать отдельно.
В противном случае код этого посоха будет уже почти god-object.
Фаербол (как тут уже писали) сам по себе персонаж порой. Так как маг конечно кастанул, создал объект. Но вполне возможно, что этот объект просто неспешно куда-то полетел. От него могут увернуться, его могут уничтожить и тд и тп. Вот когда он попадет, тогда урон и будем считать, от "кулака" фаербола.
Если фаербол это только эффект, а удар всегда наносится, то это с точки зрения расчета ничем не отличается от удара мечем/кулаком кроме радиуса действия.
Дамаг наносит не рука, аура и не персонаж. Оружие. Всегда оружие, остальное — модификаторы, которые должно опросить оружие.
А если у нас ДВА оружия в руках? Например меч и кинжал или меч и щит (который тоже в определенных ситуациях используется как оружие)?
Это может негативно повлиять на разнообразие игрового опыта. Если позволить игроку иметь оружие/щиты как модификаторы к тому, что он делает (т.е. когда урон наносится всё же персонажем), то перед ним откроется бескрайнее поле разных комбинаций снаряжения, которое, на первый взгляд, ему даже не подходит. Зачем магу кинжал и щит, если ему и с фаерболлами хорошо? А ведь если кинжал заставляет горящих противников взрываться при смерти, а щит даёт огненному урону свойства ледяного, то фаерболлами стрелять становится в разы интереснее. И эти предметы всё ещё хороши для того, чтобы кинжалом колоть, а щитом закрываться, если вложиться в огненный урон другим снаряжением или прокачкой. Важная ремарка, что я подразумеваю фаерболл не привязанным к волшебной палочке или перчаткам, персонаж просто "умеет" его делать.
Даже если вопрос состоит в том, есть ли желание дать игроку свободу или посильнее его ограничить, я при любом раскладе адвокатирую за нанесение урона от лица персонажа, даже в простеньких системах итемизации/навыков это может добавить приятной остроты
В ООП-версии мы вдруг пытаемся «наносить урон изнутри меча»: метод Sword.hit(target) лезет за параметрами атакующего, целится в защитника, спрашивает у «брони по которой ударили» модификатор, затем уведомляет target.takeDamage(...). А если урон идёт от «огненной ауры» на перчатках? Кто «владеет» действием — меч, рука, аура или персонаж? Где должен жить код? Как не нарушить инварианты десятка объектов одновременно?
Damage getDamage(Character * defender, Weapon * weapon){
....
}
wolf.DownHealth(getDamage(wolf,sword));
wolf.DownHealth(getDamage(wolf,dager));
И что здесь не так.
Спасибо за большое количество кода, теперь мне все стало понятно. Текст отлично объясняет проблемы и даёт решение. Сарказм end.
Неужели нельзя примеры кода добавить, было, стало и т.д
Описывать код только текстом это знаете моветон. Я что должен предполагать, что вы имели ввиду и по тексту в голове код писать?
Десятилетиями нам рассказывают, что есть только два пути: громоздкие иерархии ООП или стерильная бюрократия ECS. Нас заставили поверить в то, что создание игр — это выбор между анархией и диктатурой.
Господи! Да кто с вами всё это сделал? Сколько вас там? А сколько их? Есть понимание, на каком континенте находится бункер, в котором вас держат?
У меня устойчивое ощущение, что статья оборвалась на половине пути. Думал, от упомянутых паттернов, нас проведут, через лапшу и легаси-грабли, прямиком к реликтовому и потому сверхнадёжному говнокоду, объявив его вершиной эволюции программирования. Но чуда не случилось. Вместо использования какого-либо подхода, предлагают "просто писать игры".
Всё ещё очень много команд, где во главе угла не качество кода или какой-нибудь конкретный подход, а скорость разработки. Невзирая на баги и отсутствие архитектуры как таковой. Да-да, написание легаси из коробки. А если код кривой и падает, так что с того? Всегда можно штрафануть программера за затягивание сроков. Не так ли?
Предложенный метод "Учётной системы" применяется для управления предприятиями. Объекты в справочнике участвуют в различных регистрах - состояниях (моделях). Вычисления в регистрах могут быть взаимосвязаны транзакциями. Такой подход победил ООП и не даёт разбить на микросервисы ERP системы.
ООП и микросервисы - это не про архитектуру, это про командную работу, в которой удобно за каждым микросервисом или объектом закрепить ответственного и давить на него, требуя рабочий код в рамках возложенной ответственности, глубоко не вникая в его потребности.
Учётная система - комплексный подход к оптимизации связей элеметов. Кажется не сложно продумать заранее:
Регистр состояний (перчатка новая, перчатка требует починки);
Регистр владения (какие перчатки какому герою принадлежат);
Регистр столкновения юнитов (для всех перчаток, мечей, кольчуг проверяется их пересечение в 3д модели);
Другие регистры.
В этом подходе взаимосвязанных регистров размывается ответственность. Должен появиться архитектор, которого будут клевать со всех сторон, но который сможет убедить участников проекта сохранять компромис между функциональностью и простотой архитектуры.
ВЫВОД1: Описанная проблема не архитектурная, а организационная. Когда в команде распределяетя работа:
Начинающие разобьются на независимые объекты;
Опытные уже думают о группах сервисов состояний объектов;
Прожаренные смогут возглавить разработку по упрощённым шаблонам, не распыляясь на детализацию.
ВЫВОД2: Любой шаблон - устаревшая архитектура. Придет однажды новичек с новым объектом, сломает шаблон и все начнется сначала.
Пример автора использования ООП на примере RPG игры говорит лишь о том, что автор не может в ООП.
Нет, он всего лишь неправильно описал модель предметной области - а она там весьма нетривилаьная. Хотел расписать, в чем именно, но к счастью тут в другом комментарии уже привели ссылку на перевод цикла статей от разработчика Roslyn, он там свою предметную область иллюстрировал как раз примером из RPG. Если почитать тот цикл, то выяснится, что проблема имеет весьма нетривиальное решение, и для ее решения нужно не просто мочь в ООП, а владеть им хорошошо.
Хотя вот мне почему-то решение с внешними объектами-правилами пришло в голову почти сразу. Возможно - это из-за специфического опыта: понимания как выглядит RPG в настольком варианте - там не "меч наносит удар", а "игрок объявляет Dungeon Master'у(ведущему), что он наносит удар мечом", а результат удара рассчитывает уже ведущий.
Это конечно всё классно, но "Talk is cheap. Show me the code".
Из того, что я прочитал и понял (сужу только по тексту, кода то нет), выглядит как решение для небольших/средних проектов и небольших команд.
Я просто представляю как 50 программистов на проекте, где 500 фичей в которых количество компонентов и систем иногда переваливает за полсотни, будут писать на этой одскульной парадигме.
Как будет устроена изолированная работа, чтобы к примеру минимизировать конфликты при мёрджах?
Ещё тяжелее представить, как объяснить полсотне людей идею, правила и идиомы. На любом проекте каждый программист и так пишет по-своему, но с ECS это "по-своему" имеет одинаковый скелет, схожий интерфейс для взаимодействия между фичами и какие-то рамки.
Ещё непонятно где данные держать, тот же массив Unit[] или MagicData[]. В глобальной переменной с типом GameData?)))
но вы избавлены от сложной машинерии ECS по сопоставлению Entity ID с индексами в десятках разных контейнеров. Мы получаем не меньшую производительность, но с на порядок меньшей сложностью.
Для этого существуют фреймворки, чтобы эту сложную машинерию прятать и заворачивать в общий интерфейс.
Я просто представляю как 50 программистов на проекте, где 500 фичей в которых количество компонентов и систем иногда переваливает за полсотни, будут писать на этой одскульной парадигме.
Интересно как крупные ААА игры пишутся, на олдскульной парадигме? Просто кому то ООП не нравится вот и придумают новое, а по факту все нужное уже есть😅
Как будет устроена изолированная работа, чтобы к примеру минимизировать конфликты при мёрджах?
За два года работы(пишем по ООП)не разу их не встречал, например программист мидл делает ветку от main, а джун делает ветку от мидла, да и думаю есть куча других вариантов которые при мерже не выдадут ошибок( ну и не забываем вариант ручного решения конфликтов)
Ну и не забываем что на самих движках по типу Unity было бы странно писать не на ООП, по сколько ООП предоставляет в нем так же работать на паттернах, а к изолированности классов с опытом привыкаешь и проблем это таких как в начале не вызывает)
Предыдущие ораторы всё настолько хорошо разобрали, что и добавить нечего.
Басня «Мартышка и очки» на все времена.
А проблема излишнего усложнения кода Во Имя Священных Правил книжного Высокого Искуйства (написанных теми, кто пишет больше правил, чем кода) - от способа программирования не зависит.
Да ну никто не страдает, если бы реально страдали, то выбор подходов программирования был бы более разнообразным из-за неудобств и разногласий среди разработчиков.
кто внутри кого вызывает кого
Объект управляет только своей логикой, защищает свои инварианты. Когда появляется логика, где вовлечены несколько объектов, нужно создавать третий. Если это сложно, то объекту пытаются навязать управление чужими правилами.
А обоих случаях сложность системы нарастает лавинообразно и система становится хрупкой.
При первых признаках сложной бизнес логики переходите на ФП
В ООП это будет выглядеть так
// Начали с domain model
class Character { }
// +10 фич = +50 "service" классов
class DamageCalculator { }
class DamageModifierApplier { }
class AuraDamageProcessor { }
class ElementalDamageStrategy { }
class DamageCompositionService { }
// Каждый с 1 методом Calculate() и набором зависимостей
90% связующего кода, 10% логики
v1: 5 классов, чисто
v2: 30 классов, "ещё держится"
v3: 200+ классов, никто не понимает flow
Конструктор больше метода
class AuraDamageProcessor {
private readonly IItemRepository _items;
private readonly ICharacterStats _stats;
private readonly IAuraConfiguration _config;
private readonly ILogger _logger;
private readonly IEventBus _events;
private readonly ICacheService _cache;
public AuraDamageProcessor(
IItemRepository items,
ICharacterStats stats,
IAuraConfiguration config,
ILogger logger,
IEventBus events,
ICacheService cache)
{
// 20 строк присваиваний
}
public int Calculate(int baseDamage) {
return baseDamage * _config.Multiplier; // 1 строка логики!
}
}
Так а нам же в ооп не обязательно перепрописывать поля всех классов родителей, ребенок их просто унаследует, а там можно менять параметры по своему желанию, мне кажется что с большим объемом классов ооп окажется более читаемым, ну по крайней мере это все не будет хранится в одном скрипте, если я правильно уловил суть статьи
Не сарказм ли это?
90% связующего кода и 10 логики - это прям эталон того, что делается что-то не так.
И это как раз очень плохой код на больших системах, который ещё и разрастается быстро и бесконтрольно. Как минимум, в нём всегда будет ещё много-много строчек кода условий, которые будут говорить как именно перемножать и надо ли это вообще делать, так же там появятся досрочные выходы, и выходы посреди метода(break посреди циклов и return посреди и в начале метода). Это всё как раз ошибки проектирования в OOP. В норме (почти) не должно быть выходов посреди методов, лишних связей, как и управляющих объектов так же минимально - логика внутри объекта.
Это ни разу не ООП, это как раз вариация ПП.
А зачем 20 строк присваивания?
Declare C# primary constructors – classes, structs | Microsoft Learn
learn.microsoft.com›.net›C#
Если в проекте большое кол-во классов мешает пониманию логики, значит проблема в дизайне. Если все грамотно на слои разделить, то суммарное кол-во классов в проекте никак не будет мешать пониманию логики. Потому что рабочий контекст будет ограничен набором классов текущего слоя, а вся сложность более низких слоев будет скрыта за интерфейсами.
Если у класса 20 зависимостей, то это снова говорит о проблемах в дизайне. Скорее всего, у класса слишком широкая зона ответственности и его можно декомпозировать.
При первых признаках сложной бизнес логики переходите на ФП
Было бы интересно увидеть пример рпг на ФП.
(если что, я на стороне ECS и считаю его как раз "ФП здорового человека", во всяком случае для игр).
А ты на стороне ранних или поздних ECS?
И на каком языке ты предлагаешь разрабатывать саму игру, а на каком - описывать component-ы?
Боюсь мои советы насчет языка вряд ли подойдут кому-то кроме меня - пилю игры на Crystal и на паскале, ну и немного на юнити. Во всех трех случаях ецс-фреймворки самописные на базе спарссетов (т.е. структурные изменения дешевые а скорость итерации меня не слишком беспокоит).
Раньше много лет пилил на ООП (да и без ООП в процедурном стиле тоже бывало), когда узнал про ECS это стало прямо глотком свежего воздуха. И производительность тут не при чем - дело именно в архитектуре. Любые переделки (из которых процесс создания игры в общем-то и состоит) воспринимаются намного проще - вместо монструозного цикла обработки напичканного костылями подпирающими другие костыли отдельно независимые системы, надо добавить механику - добавляем еще несколько систем. Единственный минус - бойлерплейт. Раньше я вместо компонента-тега одной строкой втыкал куда-нибудь флаг, и второй строкой добавлял проверку на него, а сейчас должен десятью строками завести компонент и систему. Но флаги и проверки по моему опыту со временем превращаются в лапшу взаимосвязей когда при любом изменении что-то да сломается, а компоненты и системы - нет.
Когда мне сейчас говорят что можно еще и убрать ецс бойлерплейт (удивительным образом эта статья совпала с обсуждением на gamedev.ru где предложили примерно тоже самое) - я бы и рад поверить, но без конкретного примера не выходит - писать по простому я и раньше пробовал, выходило (особенно когда до конца конкурса неделя и ни о какой правильной архитектуре уже речи не идет, успеть бы запилить задуманное) не очень. Да, возможно я что-то упускаю и можно писать так же как на ецс но без ецс и без бойлерплейта - если мне покажут такой способ я не исключаю что перейду на него.
Флаги и проверки посреди, в стиле, а всё ли ок с данными, их надо отсортировать, проверить вообще есть ли подходящие и т.д. - признак плохого проектирования в ООП.
Монструозные циклы не редко то же самое) Но тут не всегда, иногда всё же бывают большие циклы, но скорее там, где надо сложную обработку данных и быстродействие, например, обработку мешей.
А, ну и ECS с функциональщиной ничего общего не имеет. По структуре методов это считай процедурка. Вероятно, тебе проще структурировать информацию в ECS. В таком подходе это структурирование является обязательной частью работы, поэтому её не пропустишь - код вообще не будет правильно работать. В ООП в некотором роде схоже нужно структурировать, но можно забить и писать в процедурки
Флаги и проверки посреди, в стиле, а всё ли ок с данными, их надо отсортировать, проверить вообще есть ли подходящие и т.д. - признак плохого проектирования в ООП.
нет, флаги в стиле "этот скилл стреляет по летающим без штрафа, поэтому для него пропускаем проверку штрафа".
В ООП в некотором роде схоже нужно структурировать
На мой взгляд проблема в модификациях, как в общем и в статье написано. Если в ООП все правильно спланировать то там всё будет чисто и красиво. До первой придуманной механики которая ложится поперек тщательно спланированной архитектуры и из-за которой надо или перепланировать или втыкать костыли. А в ецс ничего планировать (во всяком случае сознательно) не надо - есть набор правил, просто следуешь им и в итоге любая переделка затрагивает только небольшую часть систем.
А, ну и ECS с функциональщиной ничего общего не имеет. По структуре методов это считай процедурка
С точки зрения кода - да. Сам ецс фреймворк подразумевает низкоуровневые мутации данных.
Но вот с точки зрения пользователя этого фреймворка - у нас есть чистые данные без методов, а вся их обработка выполняется функциями без состояния и сайдэффектов. Ничего не напоминает?
Флаг именно в таком виде, если он не нужен в ECS, может так же реализовываться в ООП. Конкретно указанный случай скорее некорректен, т.к. его быть в целом не должно.
Ну да, планировать надо. В ECS тоже. Только сам ECS подталкивает к этому. Но есть момент, что когда логика становится сложной, то наборы правил тоже становятся очень сложными. Как и отслеживание логики и последовательности изменения состояния. И часто это много сложнее чем в ООП, потому как логика действия и изменения данных проходят большие цепочки контроллеров. Чуть ошибёшься в очерёдности, не добавишь для её контроля и тп - всё поломается. Кстати, и производительность такого кода много меньше, чем в ООП.
Чистые данные без методов - это не про функциональщину) Там, пусть и условно, всегда есть методы для каждого состояния и они как раз условно содержат состояние, точнее меняют текущее. Но и выполняются они не такими группами и позволяют менять состояние целиком. А так можно и ООП назвать похожим, там тоже есть данные и применяемые к ним методы, прям как в FP, но уже есть "сайдэффекты". Всё таки разница в подходах по сравнению с DOP огромна в обоих случаях, и ещё больше в вариантах для Unity
Флаг именно в таком виде, если он не нужен в ECS, может так же реализовываться в ООП. Конкретно указанный случай скорее некорректен, т.к. его быть в целом не должно.
В ецс это будет компонент. Пример да, условный, чтобы не углубляться в конкретные детали.
И часто это много сложнее чем в ООП, потому как логика действия и изменения данных проходят большие цепочки контроллеров.
Есть такое и на мой взгляд это приемлемая цена. Потому что когда все-таки выстроил последовательность сломать ее изменениями становится сложнее чем в ооп из-за меньшей связанности. Ну и если есть независимый от всего остального большой кусок сложной логики - не вижу проблемы вынести его в одну большую систему и там обработать от начала и до конца.
Чистые данные без методов - это не про функциональщину
Если речь о чем-то типа Хаскеля, то там как раз данные отдельно, чистые функции отдельно.
Да, если писать на нем игру там скорее будет одна большая функция которая из старого состояния делает новое, но вот отделение данных от логики вполне в духе ФП.
Всё таки разница в подходах по сравнению с DOP огромна в обоих случаях, и ещё больше в вариантах для Unity
Это да, на практике ецс пользуются люди которые про моноид в категории эндофункторов и не слышали, так что разница конечно большая.
Тут вопрос чем именно ломается. Изменение в логических операторах или последовательностях всё же много чаще, чем изменение принципов. А это как раз ломает уже построенную архитектуру ECS. И такое изменение может потребовать проработки всей группы из-за высокой связности. Тут уже писал, имхо, ECS хорош как раз там, где такой сложной логики нет, а классификация вообще не требует последовательного выполнения. Но не редко сложная логика в таком виде приводит к дополнительным вычислениям и данным, дабы реализовать те же условия логики как часть данных.
Так, многие и про *-варианты не слышали) Тут знать терминологию не надо, чтобы понять суть. А она вообще в другом
ECS это один из шаблонов на базе ФП (+/-). Статья о том, что шаблон иногда не удобен.
А от меня тезис, что в узкую щель между ООП и ECS пролезет пароход
По поводу нужен 3-тий объект, чтобы связать 2 других - согласен.
По сути 3-тий класс это некий Interaction между объектами: AttackInteraction, MagicInteraction, DialogInteraction, BarterInteraction(Inventory buyerIntentory, Inventory sellerInventory, IBarterActor buyer, IBarterActor seller), LootInteraction, StealInteraction и тд. Этих взаимодействий должно быть ровно столько, сколько их есть в игре. Вычленяются они из естественного языка: Игрок атакует Моба; Моб атакует Игрока; Игрок лутает Моба; Игрок берет квест, Игрок и тд.
Т.е. есть изолированные Components: Статы, Инвентарь, Дерево Умений, Изученные заклинания, Перки, Ячейки заклинаний и тд. Например при нажатии кнопки Loot у убитого врага, открывается UI вашего инвентаря и убитого моба. При попытке забрать айтем запустится new LootInteraction(Inventory source, InventoryItem item, Inventory target).Execute()/CanExecute()
, которая вызовет target.CanPutItem(item)
и если true
, уберет айтем из source инвентаря и положит в target инвентарь.
А такие классы как у вас в примере: AuraDamageProcessor
являются "чистой выдумкой" из GRASP и по большому счету нужны просто для декомпозиции кода. Жить такие классы всегда будут где-то в той же папке, где описывается Interaction или Component.
Но не согласен, что это становится неподдерживаемым, если в проекте 200+ таких классов: сложность понимания флоу и механик для разработчика должена быть сопоставима с сложностью понимания флоу и механик для игрока. Если получилось 200+ компонентов и интеракций, ну значит игра не простая в плане механик, легкой поддержка точно не будет простой при любых раскладах.
Зато все классно тестируется. Можно просто запустить целую игровку сессию, задать стартовые состояния объектов кодом, и в рамках юнит теста, вызывать методы компонентов и взаимодействия между ними, симулируя поведение игрока и мира, и делать snapshot мира (save game) после каждого действия, и сравнивать его с ожидаемым значением. И можно запилить интеграционный тест, и то же самое провернуть с игровой сессией уже в рамках интеграции с движком.
Вор атакует тролля, маг атакует тролля, воин атакует тролля, вор обворовывает тролля, маг заколдовывает тролля, маг зачаровывает зелье, маг варит зелье, вор пьет зелье, воин метает зелье в вора, маг левитирует зелье, зелье левитации пьет тролль, тролль левитирует, воин атакует левитирующего тролля, а еще есть призраки и с ними все не так как с троллями, а еще есть священник и у него с призраками совсем особые отношения, а есть бард, и у него с призраками тоже по-особому но не так как у священника, а потом добавили мульти класс в игру и стало можно быть воином-вором или бардом-священником, а потом сделали призрака играбельной расссой и теперь появились воины-ппизраки-священники
Не вижу проблемы. AttackInteraction должен все это учесть. Он должен пройтись по текущим эффектам на цели и обнаружить, что на ней весит эффект левитации. Далее через слабое связывание (например, рефлексию) он должен посмотреть, какие специальные правила работают для левитирующих объектов во время атаки (например все атаки ближнего боя гарантированно промазываются). Далее с помощью рефлексии создаётся объект этого правила и ему прикидывается контекст взаимодействия. Првило рассчитывает шанс промаха 100%. Но он так же проверяет, есть ли среди эффектов атакующего перк IgnoreLevitationForMeleePerk. Если нет - ставим шанс промаха 100%, если да то 0% условно. И так с базовой атакой и так же с другими модификаторами. Т.е. каждое взаимодействие является скетелом механики и позволяет подключать к ней любые частные случаи за счёт Dependency Inversion и Inversion of Control.
Интерфейс может выглядеть так: IDefenceEffectHandler<LevitationEffect> { HandleEffect(LevitationEffect effect, IAttacker, IAttackTarget)
.
Т.е.если у вас такое количество комбинаций, важно создать такой скелет, чтобы можно было что угодно с чем угодно комбинировать.
Соответственно интерфейсов будет много. Одни будут считать общие правила по эффектам. Другие будут описывать правила как тролли взаимодействуют с призраками и ТД (другая плоскость/матрица расчета) IAttackerAndTargetSpecialRule<Troll, Ghost>
. Далее надо будет определить, что считается в первую очередь - общие правила по эффектам или специальные правила.
А если в комбинации и эффект и то на ком он висит и то кто атакует - можно сделать третий интерфейс. А можно добавить if в специальное правило для троллей и призраков. Идеального решения нет. Все зависит о того, насколько способ такого взаимодействия частый и достойно ли это отдельного интерфейса и встраивания в фреймворк механики, или лучше это сделать как исключение.
Ну если у нас каждый эффект независим, то интерфейсов будет просто много. А если они будут произвольно сцеплены, то мы получим комбинаторый взрыв перед фигурной скобкой, нет?
А вот точно ли оно того стоит?
Если просто написать одну большую функцию и в ней в очевидном порядке обработать особые случаи, это будет можно не только написать, но и даже прочитать потом и понять.
Надо смотреть. В общем случае число if/else кейсов будет одинаковым в сравнении с подходом написать в лоб в процедурном стиле, просто скрыто за полиморфизмом или слабой связанностью. Но именно удобство чтения и расширения будет больше, при условии, что получается смоделировать какие-то ключевые узлы механик.
Я всегда в таком случае просто пишу 2 версии и сравниваю, чтобы понять, что выглядит более удачно. Занимает больше времени на старте, но даёт возможность сравнить и принять решение. Пока что я чаще всего выбирал ООП подход, но где-то можно и процедурами сделать, если механика непонятная и весь этот оверхэд на скелет может не окупится.
Но я не разработчик игр, я могу очень многое упускать из виду.
Зато все классно тестируется
Нет
делать snapshot мира (save game) после каждого действия, и сравнивать его с ожидаемым значением
Плохой способ тестирования
Необходимо ОРП
Несправедливо критиковать инструменты, если не умеешь ими пользоваться. Сам факт использования ООП, ФП или чего-то ещё, не сделает из говнокодера великого Гудвина.
Я уже десятки команд встречал и соглашусь насчёт ECS полностью. Простые действия в несколько строк в COP(OOP) делаются через создание нескольких файлов. ECS был бы удобен именно как внутренний слой в циклах обработки - почему так не сделали с теми же MB совсем не понимаю.
В большинстве случаев, когда говорят о том, что в ООП плохо, не получается и т.д, и тп = команды просто не могут в ООП. Не могут заранее спроектировать и поддерживать продукт в случае изменений, не умеют этого. Далее опишу основные встречаемые проблемы/
Вот буквально недавно ещё 2 "закатных" проекта видел за последние 3 месяца. В 1 MVP. Во 2 "чистая архитектура" + MVP + ещё message bus на все вызовы(методы) бизнес логики... И вот одна из самых криповых и очень распространённых практик во всех подходах разделения кода на M - M остаётся без логики, чисто данные(или почти так). Это слабоумие преследует команды уже 15 лет! Не так давно я думал, что это уже ушло в прошлое, но нет, это всё ещё часто даже у разрабов с опытом в 5 лет встречается, что другого они и не видели.
Вторая проблема - подходы из других областей. Приходит очередной вебщик, CRM-щик и т.п. И тащат свои системы в Unity. Контроллеры на каждый чих? MVC в худшем его извращённом представлении, где M данных, а С логики. Или просто контроллёры всего подряд, что фактически PP. Видел это десятки раз.
Сейчас ещё почти повсеместно использование DI. Но про контейнеры даже не слышали. Как сделать массовое создание объектов. Вместо удобного управления зависимостями выходит god-контекст. Портянки зависимостей на сотни using. И так почти все проекты. Т.е. люди не научились с ООП зависимостями работать, а тут им подогнали крутой инструмент где их вообще вертеть можно как угодно. Вот они и вертят. Пока не встречал ни 1 проект, где с зависимостями хорошо работали в Unity. Часто наоборот, это было ужасно.
Чуть в стороне, но не менее часто ломающий логику ООП и производительность - повсеместно используемый UniRx. Каки другие плюшки из других систем, которые влияют на связи между методами и тем более классами.
А, ну и не достаточно просто сказать - мы делаем M***. Так не работает - нужно нормально проводить дизайн фич, проектировать. Это надо в любой парадигме. А ООП, похоже, сильнее чем прочие зависит от этого. Буквально, всё только в это и упирается - не провели дизайн/проектирование классов и зависимостей, то считайте у вас и не ООП. Да, с опытом можно это делать на ходу, но сначала нужно этот опыт получить.
разделения кода на M - M
Что такое М - М ?
Сейчас ещё почти повсеместно использование DI. Но про контейнеры даже не слышали. Как сделать массовое создание объектов. Вместо удобного управления зависимостями выходит god-контекст. Портянки зависимостей на сотни using. И так почти все проекты. Т.е. люди не научились с ООП зависимостями работать, а тут им подогнали крутой инструмент где их вообще вертеть можно как угодно. Вот они и вертят. Пока не встречал ни 1 проект, где с зависимостями хорошо работали в Unity.
Это неспроста, верно?
Service Locator антипаттерн, просто спрятанный за DI-фасадом. Всё доступно для всех. А раз доступно, то изменения/эффекты могут быть неожиданными
О том и речь - не научились ещё в ООП, а уже используют инструмент, что его ломает
Думаю, наоборот.
Не пользуются "правильно" ООП потому что не удобно. Всё норовят костыли использовать. А не удобно потому что ООП
Любая новая когнитивная деятельность поначалу неудобна) ECS так же поначалу часто неудобен почти для всех, не важно что там с ООП. Да даже само по себе программирование то ещё неудобство)
В таких вещах хорошо смотреть как удобно это читается, чинится, изменяется. И вот в первых двух неудобств у ООП нет вовсе. Изменения посложнее, но если не требуют перепроектирования, то так же крайне просты. И, что не маловажно, изменения совпадают с дизайном(ТЗ) большинства людей.
К сожалению, работать часто приходится в команде, подчиняясь ритуалам, навязанным неким локальным божеством. Например, я долго воевал с тимлидом-фанатом Terraform и его какой-то маниакальной настойчивостью завернуть в модули любой TF код. Даже там, где просто создание ресурса даст результат здесь и сейчас, а не через несколько дней. Я думаю, особенность плодить абстракции, слои и обертки там, где они не нужны - это психическое заболевание. Или по крайней мере, внутренний страх отклониться от канона, предписанного когда-то давно вышедшим в тираж авторитетом.
Бардак головного мозга. Чувак заменил одну объектную модель на другую, и сделал вид, что это уже не ООП. Как ни крути, системное мышление, по определению, оперирует абстракциями объектов, их взаимосвязей, состояний и поведений, образующих целенаправленные взаимодействия. Какую бы сказочную парадигму имплементации ты ни выбрал, везде будет то же самое, нужно только уметь создавать качественные модели, адекватные решаемым задачам, а не ограничивать себя искусственными ментальными заборами, ограждающими маркетинговые экосистемы.
Видел когда-то на Хабре серию переводных статей с частично схожим содержанием - начинали с классического ООП дерева в игре с классами вроде Player, Monster, Sword и постепенно переходили к другому типу ООП композиции фокусирующейся на игровых правилах и обработке данных в соответствии с ними. К сожалению даже ключевых слов для запроса в поисковик в голову не приходит. Может кто-то помнит ее?
А если я не страдаю от ООП, можно не выбрасывать?
Статью не читал, извините.
Да кто страдает то, сейчас только и делают что от ООП отказываются, все никак отказаться не могут
Просто ООП переходит на следующий уровень "абстракции" - обмен флажками &|~, очередями-семафорами, помимо вызовов функций по указателю, наборов switch-case (под катом иерархий классов) или страшный сон dynamic/reinterpret/static_cast.
Ключевое это то, что чтобы сделать хорошую игру обычно всё должно быть связано со всем. Иначе не получатся хорошие механики
А это уже приводит к тому что подходы из других частей программирования никак не лезут, все абстракции обязаны друг в друга протекать
Так что общий подход к написанию игр это создать сущности (например юниты, поле боя), потом создать алгоритмы работающие над сущностями (например нанесение урона) и дальше всё делать с помощью этого "движка"
Очевидное решение а том что взаимодействие между объектами это должен быть отдельный объект. То есть к примеру юнит дает заявку на взаимодействие с другим юнитом. Некий супер арбитр выдает ему и его противнику объект взаимодействия. Дальше объект взаимодействия оценивает заявку и производит взаимодействие всеми зарегистрированными способами и проверяет нет ли необходимости вовлечь еще и другие объекты в это взаимодействие.
Проблема в том что возникает проблема синхронизации, особенно на многопоточке, появляются дополнительные проблемы с памятью в виде дёрганий new-del на сотнях байт, массивы-списки объектов для динамического обновления. Разукраска объектов полями ID, дублирующих внутренний тип, всевозможные флажки состояний и слоты (void*) для неких сущностей с проверкой на NULL. Вообщем геймдев уровня mov [edx],ebx уже смотрит на ООП как нечто что должно генерироваться само по себе неким Regexp-ом из файла настроек.
Зависит от правил и структуры игры. Выделение памяти для любого мидла в геймдеве решить не проблема. Массивы-списки это ещё не плохо. Сейчас не редко реактивку пихают. Поля ID вообще тут при чём? Обычно это появляется при подключении БД.
Геймдев уровня mov [edx],ebx умер больше 30 лет назад. На уровне движков в случае особых оптимизаций это ещё можно встретить, и то, не мала вероятность что за такой код тебя побьют. И правильно, т.к. компиляторы не плохо работают.
Всё верно, что за такой код надо бить не того кто его применяет, а кодогенератор. Фактически геймдев не может быть универсальным инструментом. Использование многопоточки, различных битовых полей (это на предмет ID) и микро-виртуальных машин поведения объектов, расширений MMX/SSE/AVX и прочих CUDA-подобных расширений для физики, порождает необходимость иметь нечто большее чем makefile, равно как и оптимизация запросов в память для мелких сущностей, это может отобрать в реалтайме большую часть времени, включая копирование GPU-CPU. Иными словами эффективный фреймворк уже содержит кодогенераторы которые вобрали в себя всё это дело и ООП (согласно вопросу в теме) здесь является скорее методом документации чем непосредственно самой реализации, так как в конечном итоге там будет мешанина из asm/C/C++/OpenCL, созданная ползунками и флажками в Tk GUI образно говоря а для конечного юзера оставлен интерфейс в виде статически связываемых прототипов, коллбэков и виртуальных функций.
А вообще все это тупиковый путь. Благодаря описанным в статье подходам игры превратились в идиотскую вселенную с шаблонными взаимодействиями. где например сундук можно открыть только одним способом и с одним и тем же звуком. Все взаимодействия в мире легко описываются с помощью моделей частиц и полей. Математическая база для, всего этого давно придумана. Не нужно сильно дробить мир на частицы. Достаточно атомарной детализации на уровне майнкрафта, обернутой в гладкие оболочки. И тут сразу заработают и звуки и свет и разрушения.
Да, интересно. Всегда в веб разработке и в прикладных задачах использовал ООП и думал, что не к месту оно здесь, но наверно в игровой индустрии оно заходит на ура. А вот оно как оказывается.
Я не страдаю с ООП. У меня в нём всё прекрасно и логично.
Если бы допустим, вы пытались создавать программы не руководствуюсь принципами ООП, и тонули в запутанном коде, а потом открыли для себя ООП и все бы стало ясно и понятно, то тогда это было бы аргументом в пользу ООП.
Но если изначально научились программировать в рамках этой парадигмы, то тогда откуда можно понять, что она полезна а не просто дополнительное ограничение, ни для чего не нужное? Кто знает, что было бы если бы вы никогда не слышали о пользе ООП, и писали как получится? Был был код хуже или лучше?
Шанс проверить это имеют только те, кому за 50 - они научились программировать до того как парадигма ООП стала популярной. Мне за 50 и я не помню момента или примеров, когда ООП все упростила. Напротив, помню как все экспериментировали, перегибали и в конце концов просто привыкли к ООП, так и не понял дало ли оно какую-то прибавку в понятности и сопровождаемости кода.
Возможность упаковать код и данные по классам она несомненно удобна, просто из за вопросов нейминга и сокращения количества входных параметров в функции.
Но дальше говорят, что god-объект это плохо, анемичная модель - это плохо. И здесь никакие реальных доказательств что это плохо я не видел.
Зато обратных примеров видел множество. Когда то что можно написать в 3 простейшие процедуры, предельно понятные, пишут в 10 классов, взрывающих мозг.
Но я не хочу его выбрасывать. У меня с ним нет проблем.
Возможность упаковать код и данные по классам она несомненно удобна, просто из за вопросов нейминга и сокращения количества входных параметров в функции.
Это весомый плюс.
Но дальше говорят, что god-объект это плохо, анемичная модель - это плохо.
Нет, это не плохо, если вам действительно понадобился именно такой объект. Не слушайте других. ООП - инструмент. Как им пользоваться - решать вам.
Ну пример упрощения от ООП, допустим, можно назвать: оконный интерфейс и система меню. И если вы помните, первое ООП в массы у нас шло как раз с TurboVision. Так в принципе можно наковырять ещё примеров. Но в целом ваш ход мысли разумен.
Народ, а вот скажите - кто нибудь книги читал по ООП из тех кто пытается сказать про что оно?) Можно мини соц опрос
Приведите пример книги которая по вашему мнению про ООП, и мы ответим. Или прочитаем.
Давайте хотя-бы с банды четырёх начнём.
А, паттерны. Я думаю эту все читали. По крайней мере на собесах все отвечают.
Я вот и удивился зачем название книги спрашивать, вроде все знают, отвечают по паттернам или основным идеям ООП?
Обычно самый модный вопрос на собесе - приведите примеры из последних ваших проектов как вы использовали паттерны GOF.
Или если нужно завалить "... но только не GOF".
Что касается моего опыта, то если я пишу с нуля сервис или библиотеку на 10 тыщ. строк кода, то там будет ООП, в том смысле что обработка разделена между классами, половина из которых будет почти полностью static. Но скорее всего там не будет ни одного паттерна. Может один или два в лучшем случае.
ИМХО, паттерны в 9 случаев из 10 нужны для решения проблем, которые разработчик создает сам себе, пытаясь придерживаться определенных концепций в коде, без ясного осознания что вообще эти концепции упрощают и насколько сложно все бы выглядело без них.
И если вы помните, первое ООП в массы у нас шло как раз с TurboVision.
При том, что в коде TurboVision едва ли был хоть один паттерн проектирования. В паскале то и интерфейсов тогда не было. И как мне показалось после многих попыток, библиотеки компонентов UI это чуть ли не единственный вид кода, где совмещение данных и поведения в объекте действительно хорошо работает.
Когда то что можно написать в 3 простейшие процедуры, предельно понятные, пишут в 10 классов, взрывающих мозг.
Один из лучших ответов на вопрос "как можно охарактеризовать ООП?"
Но дальше говорят, что god-объект это плохо, анемичная модель - это плохо. И здесь никакие реальных доказательств что это плохо я не видел.
Как правило, попытки что-то доказать у адептов "паттернов" приводят к выплевыванию штампованных фраз и лютой демагогии с коронным аргументом в финале: "А вот на больших проектах..."
И эти люди себя инженерами обзывают...
Это, конечно, смешно, но практически все, наиболее очевидные потенциальные удобства от применения ООП, были объявлены антипаттернами.
15-20 лет назад про ООП много говорили. И почти никто не умел. Я это понял чутка позже. Сам я писал вначале в процедурном стиле. Первые компроекты вообще целиком процедурками были. Лет 5-15 назад я прям смотрел на проекты и видел Успешные проекты в геймдеве именно в процедурном стиле. Много видел разговоров про ООП и тп., а проект всё те же процедурки, но с финтифлюшками, функциональщиной, реактивщиной и т.п.. Последний такой видел в распространённой мидикорке или казуалке с прокачкой особняка и т.п. До асинков и аналогов ещё часто встречал колбечное программирование - отдельный вид мазохизма, который, к счастью, совсем почти вымер. Это не как ООП в C# с event-based в норме в то время, или промисами java, а часто именно процедурный стиль
Так вот даже тот последний проект в процедурном держал дубли кода, тащил кучу зависимостей, контроллеры пухли очень сильно и разбираться в этом было так сложно, что разрабы тупо забивали на поиск кода(сотни или тысячи строк сильносвязного кода в 1 классе). Логика была размазана в контроллере, из-за чего происходила циклическая правка багов.
Но логика не была размазана между контроллерами, легко было отследить что откуда стартует и что изменяет, и в целом была на месте. Т.е. изначально проект грамотно спроектировали. Только в целом понять как оно работает было почти невозможно, не разбирая код по кирпичику до самого основания.
Потихоньку сконвертили многое в ООП и проблема с пониманием как работает та или иная вещь полностью ушла - это стало легко посмотреть прямо в коде, не спрашивая дизайнеров о том, что делали пару лет назад люди, которых уже давно нет. Зависимостей так же стало в разы меньше.
Многие из процедурного так и "не выросли". Не знаю, может даже большинство. Я на собесах ни разу не встречал вопросов о проектировании, например. Инструменты ООП - этого полно. А вот как их применять - почти и не помню такого. Впрочем, по паттернам так же. Вопросы о паттернах есть, а об их применении, условиях, последствиях - нет. Сами собеседующие часто не особо шарят. И сам когда спрашиваю, да хотя бы об алгоритмах, редко кто может сказать про общие принципы, хотя за плечами бывает явно не плохой бек и не мало проектов.
Про 10 классов - это как раз часто не смогли, кроме некоторых случаев подходов, типа интерфейс как контракт
Потихоньку сконвертили многое в ООП и проблема с пониманием как работает та или иная вещь полностью ушла - это стало легко посмотреть прямо в коде, не спрашивая дизайнеров о том, что делали пару лет назад люди, которых уже давно нет. Зависимостей так же стало в разы меньше.
Тут вопрос, что за конвертация. Просто разложили логику процедур по классам как по папочкам, инкапсулируя промежуточные данные и получая сервисы-хелперы из контейнера. Или следовали тем самым спорным практикам ООП, когда данные и код связываются вместе, никаких switch/case и все на паттернах.
Споры обычно вызывает приверженность ко второй части. Первая часть с ней все норм.
А в геймдеве используют подход из DDD, когда бизнес логика описывается отдельным классом-хэндлером? Грубо говоря, это могло бы выглядеть как то так, например юнит А атакует юнита Б, есть класс AttackUseCase(unit a, unit b, HitParameters hparams).Handle()? Тогда не возникает проблем куда запихать логику атаки. Разные виды атак разные классы (возможно наследники от базовой атаки). Или ECS про это и есть?
нет, ецс про другое. А проблема подхода в том что возникают особые случаи. Когда надо вот точно такую атаку, но с дополнительной логикой. Потом такую но с другой дополнительной логикой. А потом, например, ВНЕЗАПНО, надо третью атаку в которой есть дополнительная логика и из первой и из второй.
Как раз в этом случае данный подход и хорош, надо такую же логику но чуть изменённую, создаётся класс AttackWithBitDifferentLogicUseCase(например AttackByAxeEmpoweredWithMagicUseCase и тд), наследуется от базового. Паттерн Шаблонный Метод можно так же использовать для изменения поведения в нужную сторону. Либо уже использовать Стратегию, если явно прослеживается необходимость комбинировать какие-то варианты.
если явно прослеживается необходимость комбинировать какие-то варианты.
нет, заранее ничего не прослеживается - всё гладко, унаследовали варианты, а потом, как я сказал, внезапно оказалось что надо комбинировать. И что - всё переделывать? И это не только к типам атак относится - спеллы, эффекты предметов, влияние перков на статы, в общем везде может оказаться что нужно было комбинировать, а может и наоборот оказаться что даже наследование было оверкиллом и лучше просто энум сделать т.к. элементов ровно три и на каждый полигры завязано.
И что, в каждой новой игре вот так, ничего неясно? никаких паттернов не прослеживается?
Почти в любой сложной программе так. Только с типовой штамповкой типа сайтов-визиток, студенческих дипломов или интернет-магазинов все от начала до конца гладко бывает.
Ну вот не знаю, у нас в корпоративном секторе все хорошо относительно. Примерно понятна архитектура, понятно какие будут бизнес энтити и бизнес правила. Опять же DDD подход сильно упрощает реакцию на неожиданные требования. Опять же хорошее знание паттерны проектирования, многие проблемы с ООП снимает ещё на старте. Поэтому для меня начало статьи где описывалась объектная модель игры и проблемы подхода показались немного детскими.
Ну вот пилили продукт было все гладко. Тонну функционала сделали.
А потом контора решила выйти на международные рынки и новое первое же требование - сделать локализацию продукта. И начинается боль.
Второе требование - сделать поддержку цен в нескольких валютах. И без полной переделки всего уже не обойтись.
К сожалению когда изначально некоторые вещи не предусмотрены, то их добавление сродни написания заново.
А что у вас тексты прям захардкожены были? А цены тоже?
Ну если первоначально о локализации не думать, то не хардкодить тексты = писать в два раза больше текстов. Сначала как константу в коде, потом как текст в ресурсах. И нафига это удвоение на ровном месте?
Цены там врядли были прям захардкожены, и одну колонку в таблицу цен тоже добавить не проблема, а вот прокинуть еще один параметр в каждую функцию, которая работает с деньгами это кажется хороший такой рефакторинг.
А на чем гуй писали если не секрет, я просто не припомню когда мне последний раз требовалось тексты в коде хранить. Обычно это либо XML, HTML либо встроенный ещё какой либо формат независимый от кода, поддерживаемый фреймворком.
А как ты в коде говоришь "надо выдать это сообщение"? не текстовой константой?
Ну то есть в коде say("message12"), а потом в ресурсах "message12":"Поздравляю тебя, Шарик", нет? Ну то есть та самая тройная работа.
В тех фреймворках в которых я работал, поддержка языков уже встроена или добавляется с пол пинка. Ну например WPF, или старый добрый WinForms. Все тексты уже лежат в ресурсах на нужных формах в 90% . Нужен новый язык, создаётся новый условно пакет форм и там можно рисовать новый язык и даже внешний вид. Поэтому я и поинтересовался на чем вы так споткнулись?
Это характерная ситуация для хорошо изученной области, где последнее новое и необычное было появление возможности оплачивать через интернет.
В играх тоже такое бывает, когда делают еще один симулятор ходьбы на unreal, или еще одну веселую ферму для соцсети.
а вот когда хотят сделать невиданной красоты графику, небывалого размера и проработки мир, немыслимого разнообразия сюжетных поворотов и гораздо увлекательнее, чем 10 других современных популярных RPG, приходится придумывать новое, экспериментировать, проверять и срочно переделывать, следить за трендами и тоже срочно переделывать если что
Если вдруг на пол пути захочется условно из РПГ сделать FPS, то да тут ООП пасс :) да тут и нет простого пути. Любая модульность будь то ООП либо просто разбиение кода по си файлам уже вносят структуру, а любую структуру ломать тяжело, кроме конечно самой базовой(гейм луп, графика, физика, звук, управление).
Касательно цен, то можно и не прокидывать в каждую функцию новый параметр, как вариант можно DI контейнеры использовать, в корпоративе это по-моему уже мастхэв в проектах.
Если компания и команда делала продукты только для одного языка, то да - захардкожены обычно. И даже наличие во фреймворке всего готового может не использоваться ибо зачем эта лишняя суета?
С ценами тоже не все так просто. Нужно добавлять контекст с какой валютой хотим работать, какие отображать. Была одна цена, а теперь надо рассчитывать и показывать обе цены - это смена API и модели данных и UI. Цепляет кучу всего. А там где разные цены, то и расчет тоже может отличаться (нужно подтягивать справочники кросс-курсов, могут быть разные налоги и т.д.).
Отображать тоже нужно уметь поразному (символ валюты до или после цены, к примеру).
По опыту и в бизнес секторе очень часты внезапные изменения, когда те же бизнес-сущности внезапно меняются и добавляется куча новых, а старые наоборот становятся не нужны. Хотя бы потому, что пользователи зачастую сами не знают чего хотят и понимают это только получив и попробовав первый вариант программы в работе, как бы ни старались аналитики над начальным ТЗ.
И если по каким-то причинам переделывать программу не готовы, то пользователи, если они не бессловесные исполнители, а сами достаточно влиятельные люди в бизнесе с полномочиями, вполне могут тупо отказаться ей пользоваться, видел и такие примеры с мертвыми проектами и частями проектов.
Ну или будет кривая и страшная программа которой пользователи будут пользоваться через боль и страдания проклиная разработчиков. Видел и такие примеры.
Вплоть до того что пользователи категорически отказывались переходить с DOS программы с тестовым интерфейсом и локальной базой в dbf вроде или чем-то в том же духе на современное приложение на Angular и Symfony. Ну конкретно в том случае их с большим скандалом продавили и принудили.
Безотносительно ко всем более техническим моментам которые здесь обсуждают, просто на уровне общей логики.
Что-то прослеживается, но внезапные повороты которые рушат продуманную архитектуру - не исключение а норма.
Что забавно, когда пилят в команде еще можно пожаловаться что это придурок гд придумывает всякую хрень вместо того чтоб по нормальному использовать уже сделанную архитектуру, но когда ты один пилишь и сам себе геймдизайнер, то твои собственные идеи ровно также уходят чтобы как будто специально плохо лечь на существующую архитектуру.
Везде ясно, что принцип Лисков все убивает. Если ты даже квадрат не можешь от прямоугольника отнаследовать, то какое наследование в более сложных комбинациях?
А можно тут подробнее, чем принцип Лисков плох? И зачем наследовать квадрат от прямоугольника? )
Он хорош, и запрещает наследовать похожие, но слегка отличающиеся вещи. Например мага и воина от игрока. (см приводившийся выше цикл статей)
А если оставлять для всей череды похожих в родителе только реально общее то останется Object. И даже если у тебя будет не Object, а нечто с одним уцелевшим реквизитом, то в следующий момент у тебя в требованиях окажется еще один похожий объект, но без этого реквизита.
Если я правильно помню этот принцип, то грубо говоря он выставляет нам только одно требование: использование любого наследника в качестве базового класса, не должно ломать программу, то есть неважно какого наследника мы подсунем, поведение кода не будет сломано. Если алгоритму все равно, маг это или воин, то и принцип не нарушается. Например, рендеру все равно кто это, главное чтобы у него например был метод draw, например. Там где есть разница, там и принимать нужно либо мага либо воина.
Вот в цикле статей, про который говорилось выше и рассматривалось, что если начнем с логичного игрок со свойством оружие и потомками воин и маг. А потом когда воин не может носить оружие мага мы не можем иметь в игроке поле "оружие".
Так как сказать Игрок<воин>.Взял(Оружие<посох>)
мы не можем, а ради Игрок.Взял(Оружие)
мы и делали игрока. Поэтому Взял(Оружие)
из игрока выкидываем. И так все остальные свойства по мере развития ТЗ.
Прошу прощения цикл статей ещё не читал, но все же спрошу, а чем плох вариант когда Игрок.Взять(Оружие) просто ничего не делает(если он Воин и оружие Посох-dynamic cast, либо добавляем ВзятьОружие через паттерн Визитор)?
В статьях разобрано, чем кончается динамик каст когда палладины пытаются бить оборотней на святой земле, и каст не знает идти ему по ветке Палладин, монстр, святая земля
или по ветке Палладин, оборотень, неважно где
Кажется взять оружие которое не работает как сеттер для оружия такое же странное поведение, как
Прямоугольник<квадрат>.УстановитьА(15)
Прямоугольник<квадрат>.УстановитьБ(20)
S = Прямоугольник<квадрат>.Площадь()
Чему S равна? 400 или 225, так как .УстановитьБ(20)
неожиданно ничего не сделает?
(в угловых по прежнему реальное значение, а не часть синтаксиса)
Я вам наверно уже надоел, но позвольте ещё позанудствую: в примере с прямоугольником и квадратом все от требований зависит. Ваш пример искусственный. Если мы смотреть на него в общем смысле, то поведение SetB, SetA и GetS(), скрыты и могут быть какими угодно, гарантий что сделав SetB он не посинеет и не взорвется нет никаких. Так же GetS() никак не зависит от реализации вызовов SetA, SetB иначе мы раскрываем слишком много знаний, важно что у него есть GetS и мы можем его вызвать. В любом случае если в конкретной модели квадрат не может быть прямоугольником то и не нужно его таковым делать. Так же не вижу ничего плохого в "ничего не делать" для воина, так как во-первых это ничего не ломает, во-вторых мы всё-таки что-то сделаем, например кинем Событие и возможно покажем игроку уведомление.
Пример с квадратом - это энциклопедическое объяснение принципа Лисков.
Я просто привожу один в один аналогии с воином/игроком и утверждаю, что если квадрат с функцией вычисления площади нельзя делать потомком прямоугольника (хотя все знают из геометрии, что квадрат это прямоугольник), то и воина с магом (оба с оружием) нельзя делать потомком игрока с оружием. Так как работа с оружием у них разная, так же как работа с площадью у прямоугольника и квадрата.
А дальше следующий шаг - у нас при разработке игры и буйстве дизайнеров очень мало надежды на то, что любая функция в игроке не будет переопределена самым противоестественным образом в одном из его потомков. Поэтому строить наивную иерархию потомков игрока как специализаций общего класса игрок нельзя. Так как в связи с требованиями дизайна у них могут появляться не только новые возможности, но и новые ограничения. А это запрещено принципом Лисков.
В цикле статей(маг и воин, 5 частей) такое наследование как раз и должно быть. Он в 5 части раскрывает как это сделать без боли, точнее почему это работает и в чём ошибка была. Но примера в коде не даёт
Юзкейсы из чистой архитектуры. DDD про другое, хотя те же юзкейсы можно сказать там есть в Application Layer. Смешивать эти 2 подхода странно.
Но применение в таком виде это уже за гранью обоих этих подходов) В обоих случаях БЛ должна быть в модели, в ЧА собсно модель, а в DDD это разные части доменного слоя, как аналог модели это сущности и их агрегаты
В играх часто выходит очень сложная логика, которая не редко умещается буквально в несколько доменов, а то и в 1. Т.е. DDD там по этой же причине толком не применяется - излишне. Чистая архитектура примерно то же самое, хоть она попроще, поменьше, всё равно слишком избыточна.
Общие подходы с разделением на схожие части и связи в хороших проектах есть всегда и +- соответствуют этим 2, только без бойлерплейта. Тут почаще можно встретить MVC, например, ну или MVVM. Этого разделения + чуть-чуть смысла, и часто хватает за глаза. Основная сложность всё равно остаётся внутри логики
Я разделяю озабоченность автора статьи: действительно, слепой догматизм, будь то "ООП навсегда" или "ECS ради ECS", способен привести к излишней сложности, перегруженной архитектуре и отвлечению от сути создания игры. Однако я не согласен с идеей, что простая модель с главным массивом и наборами функций является универсальным решением.
ECS обеспечивает гибкость и масштабируемость, которые трудно воспроизвести в предложенной модели. Вы советуете использовать плотные структуры Unit[] и редкие поля в пулах с простым конвейером обработки. Такой подход может работать в простых сценариях, но он плохо масштабируется в ситуациях, где игровые механики динамичны, многокомпонентны и разнообразны (пример - Space Station 14). ECS позволяет на лету комбинировать характеристики, достаточно прикрепить к сущности нужные компоненты без переписывания структуры Unit. При этом системы автоматически обрабатывают только сущности с актуальными компонентами, не засоряя основной "скелет" игры.
Что касается производительности и локальности, то они не являются догмой, а скорее одним из ключевых преимуществ ECS. Да, в статье верно отмечено, что лишний указатель не всегда критичен, а ECS может перерасти в бюрократию. Но грамотно реализованная архитектура на ECS использует удобное хранение компонентов и обеспечивает эффективные проходы по данным (часто с поддержкой SIMD) автоматического параллелизма и компактного кэширования. Это особенно важно в сложных симуляциях и при большом числе однотипных сущностей.
Критика ECS за скрытый порядок систем и "неявные протоколы" справедлива лишь для плохо организованных проектов. При правильной архитектуре порядок выполнения систем можно задавать декларативно, документировать и тестировать каждую систему изолированно. В модели же с "одной функцией, которая всё сама обрабатывает" логика переплетена в макароны, что усложняет тестирование и внесение изменений.
Ещё одно важное преимущество ECS - чёткое разделение данных и логики. В модели с "всё в одной структуре" легко скатиться в объекты -прародители или монолитные функции. ECS же разделяет компоненты (данные) и системы (логику), что помогает держать код чистым и модульным, соответствуя принципу single responsibility.
В итоге, когда проект небольшой или находится на стадии прототипа, минималистичный подход может быть уместным. Но в масштабных играх, где сущности динамически приобретают способности, эффекты и состояния, необходима архитектура, позволяющая гибко и безопасно управлять логикой. ECS предоставляет именно такую возможность, предлагая зрелый подход к организации игрового кода.
Все верно, но я утверждаю что все это можно получить и без ECS, а бонусом можно получить большую производительность, но, конечно, это не везде важно.
Когда у тебя объекты из РПГ, где каждый имеет свою особую логику, то легко увидеть, что SIMD работает на 1-2 объектах, а дополнительные цепочки вызовов функций на порядок больше, потому как создаётся огромное кол-во классов с ~1 логическим действием.
Про критику не соглашусь. В некоторых случаях порядок не важен и так можно выстраивать архитектуру - это лучший способ. Но так не всегда получается, и тогда нужно иметь чёткий порядок. А если не получается в условиях типа case в ООП, то нужно вводить дополнительные переменные и делать лишний прогон.
Да и в целом, как задать порядок в сложной логике, которая описывается несколькими документами? Я тут не на столько компетентен, но сейчас подозреваю, что лучше вводить булевы переменные для таких случаев, а не контролировать порядок явно. А это падение производительности, но сильное упрощение проектирования.
О какой эффективности проходов по данным говорить, если контроллер требует действий над 1-10 объектами? Тут не то, что SIMD, тут вообще такое выполнение убивает эффективность использования кеша проца, и особенно предсказаний. А где прям много объектов, ну хотя бы 1к+, выигрыш действительно существенный. Но много ли таких ситуаций? До ECS такое решали просто - массивчик, проход по нему внутри цикла. И тот же SIMD можно обеспечить. Хранить и пользоваться такими данными сложно, но часто это конечная логика для C# кода, так что можно было и без ECS запустить 10к юнитов с анимацией на Unity PC. Схожим образом, например, работают(или работали) партиклы.
Про "гибко и безопасно управлять логикой" вообще мимо. Именно с логикой(условиями), у ECS прям проблемно. Перелопатить кучу данных - это да, легко. А вот управление логикой разбивается на кучу частей, которые могут очень неожиданно отработать. Фрагментация логики приводит к неожиданному поведению систем в целом. Что уж, порой без некоторых новых инструментов отладки это было вообще не реально. Хотя иногда это даже в плюс, но не когда требуется чёткое поведение. И тем более, не когда читаешь код и тебе человеки говорят о баге, а ты ещё и не знаешь с сотню систем которые затрагивают это место.
По мне так ECS хороша для какой-нить RTS в части логики юнитов, рендеринга в целом и им подобных. Т.е. где все действия ограничены и почти не содержат условий в принципе или условия сами проистекают из их особенностей(классификации).
Про детали уже сказали до меня, при этом энтузиазм чувствуется - уважаю)
Хотите перестать страдать? Разберитесь уже, что такое ООП. И это не "громоздкие иерархии". Это даже не про классы.
Помню лет 10 назад, видел подход, когда классы строились по типу sql таблиц. По сути это и был набор массивов объектов с ссылками на другие массивы.
Такой подход правда был удобен в некоторых случаях. Например, когда сущность в игре может иметь разные поведения и состояния. Например, мобы могут иметь разные типы атак и их серии. Такой подход позволял это делать без зоопарка из наследований и костыльной реализации стратегии с контекстами в контексте. Однако, такой подход имел свои проблемы, в других реализациях, например для написания змейки или тетриса, где 90% кода - это бизнес-логика такой подход бесполезен.
В общем, судить о подходах к проектированию и архитектуре вне контекста конкретных проектов странно.
Нытьё слабого программиста который "не осилил".
паттерны , грамоздкие иерархии наследования , UML диаграммы - это всё подсластители - "мы за вас подумумали и вот решение" и если вы не понимаете чего вы хотите добиться своим кодом и что лучше использовать в конкретном случае - вы будете рассказывать как всё плохо и не удобно.
Но стоит подумать о системе на уровне абстракной логики, чётко понять чего вы хотите добиться , разбить на сущности и алгоритмы , разрезать по интерфейсам - внезапно всё становиться сильно логичнее , более гибким и удобным.
Вы ругаете "инкапсуляцию" - а вы задумывались почему так ? почему все говорят что это must have, если это так не удобно ?
Аргументы про производительность вводят в недоумение - это трейд-офф о котором вы знаете выбирая платформу для разработки , как ползунок между производительностью и гбкостью со скоростью разработки. Может стоит отказаться от C++ в принципе и использовать ассемблер , там вы получите чистоту , непревзойдённую производительность и много ещё положительных эффектов , но , почемуто, это не рассматривается как валидная опция
Ваша точка зрения на ООП, например, сформирована через опыт С++ - но это не единственный и далеко не лучший представитель ООП языков, некоторые недостатки присущие С++ отсутствуют в других языках. Может когда вы рассуждаете про подходы , нужно сравнивать именно подходы , а не специфичные реализации с недостатками которые присущи именно реализациям ?
Хватит страдать: Выбросьте ООП и ECS. Есть путь проще