Войти
ПрограммированиеФорумИгровая логика и ИИ

Как должна выглядеть общая архитектура кода для тактической стратегии?

#0
(Правка: 20:25) 20:24, 23 июня 2022

Всем привет, есть задумка сделать тактическую стратегию под Unity3d.

Есть отряды космических кораблей (3-5 кораблей разных классов - допустим линкор, авианосец, эсминец, истребитель.)
У каждого корабля набор модулей - например: орудия, двигатель, щиты, радар с возможностью апгрейда этих модулей а так же повреждения их в бою.
По механике - что-то вроде древней игрушки S.W.I.N.E.

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

То до чего на данный момент додумался:
Вот например у меня класс base_ship c какими-то базовыми действиями -
родиться, умереть, переместиться, атаковать, получить урон.

я создаю класс base_battleship, который наследуется от класса base ship(или интерфейса?) и содержит в себе переменные классов base_gun, base_armour, base_engine (или реализует интерфейсы i_gun i_engine и т.д.?).

допустим есть квест - пойти на локацию и убить вражеский линкор.
я создаю на локации линкор с помощью конструктора класса base_battleship

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

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

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

экземляры класса basе_gun в случае достаточной для стрельбы дистанции до цели начинают стрельбу (вызывают свой метод "стрелять по цели", этот метод проверяет что хп цели >0, и отправляет цели параметры выстрела).

При достижении хп = 0 вражеский юнит передает  "в эфир" сообщение "я умер", после чего все атакующие его прекращают атаки.

Подскажите, если не затруднит какие видно недостатки в таком подходе, не напрашиваются ли для реализации подобной схемы какие-то паттерны? что можно/нужно дополнить, изменить?

#1
20:33, 23 июня 2022

A_Kuzmin,
если не серьезно, то посмотри ECS, для разнообразия)

#2
20:35, 23 июня 2022

nvm
> если не серьезно, то посмотри ECS, для разнообразия)

что такое ECS?

#3
20:36, 23 июня 2022

а я о чем))
unity ECS смотри в ютубе

#4
21:02, 23 июня 2022

A_Kuzmin
Нет никаких шаблонов. Каждый делает как удобно (или как было удобно тому кто начинал проект, в случае крупных). ECS - популярная и стоящая тема. Но его тоже можно очень разными способами готовить. И если такие вопросы возникают, то с ECS будет вообще взрыв мозга.
Имхо, лучше всего использовать аля-процедурный подход. Без этого классического ООП в духе "base_ship плавает, combat_ship наследуется от нео и еще стреляет", которое все равно нихрена ничего не упрощает кроме решения проблемы трудоустройства программистов. ECS стоит применять только если знаешь зачем он конкретно вам, иначе будет только хуже.

Попытаюсь развеять иллюзию. Вы, похоже, считаете что мы сейчас скажем как правильно и вы возьмете и с таким подходом типа сократите путь. Нет, так не работает :)
Если вы не знаете как делать, то надо просто делать как угодно и учиться на своих ошибках. Нарабатывать индивидуальный опыт бессонными ночами. Ну и подглядывать как делают другие (видосов куча, много проектов в опенсеорсе). Серьезно, иного пути нет. Если вам кажется что он неоправданно извилист (ведь вам всего-то надо написать пошаговую стратегию без этих всяких прелюдий) - можете сыкономить время и даже не начинать.

#5
21:39, 23 июня 2022

A_Kuzmin
> что такое ECS?

Если спрашиваешь, то тебе это точно не надо сейчас.
Из паттернов - посмотри Behavior Tree.

#6
22:02, 23 июня 2022

kkolyan
> Попытаюсь развеять иллюзию. Вы, похоже, считаете что мы сейчас скажем как
> правильно и вы возьмете и с таким подходом типа сократите путь. Нет, так не
> работает :)

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

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

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

  "Вот этот пацан ссылается на того пацана, он ссылается на экземпляр класса, который наследуется от  класса, в котором хранится ссылка на делегат, который вызывает функцию класса ... так а с чего там все начиналось, кто первый на кого ссылался? "

Ну а чтоб не изобретать велосипед решил для начала обратиться к коллективному разуму, на то что мне кто-то подарит волшебную палочку не надеюсь.

kkolyan
> Имхо, лучше всего использовать аля-процедурный подход.

Я часто вижу в интернетах мысль что ООП - фигня,  и Функциональное/Императивное программирование рулит, но пока-что опыта и знаний не хватает, чтоб понять почему.
Да и опять же Unity и C#под ООП заточено, а для меня альтернатив Unity пока что нет.
вот тут про ECS пишут, не знал про него, возьму на заметку, спасибо.

kkolyan
> Без этого классического ООП в духе "base_ship плавает, combat_ship наследуется
> от нео и еще стреляет", которое все равно нихрена ничего не упрощает кроме
> решения проблемы трудоустройства программистов.

я так понимаю, что более умно сделать так, чтобы combat_ship наследовал(реализовывал?) интерфейсы i_swimable и заодно еще какой-нибудь i_shootable ?

#7
22:12, 23 июня 2022

Гугли MVVM
https://genki-sano.medium.com/simple-clean-architecture-762b90e58d91

#8
(Правка: 24 июня 2022, 0:48) 23:12, 23 июня 2022

A_Kuzmin
а, понятно) ну ок.

Короче, под "я делаю что-то с использованием ООП" обычно называют это древнее нечто когда логику композируют путем наследования друг от друга классов с инкапсулированной логикой и данными, а также бесконечные слои абстракции. так делают, но получается мешанина, да. Поэтому и популярно мнение что ООП - фигня.

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

Юнити и C# не так уж и заточены под ООП.
C# достаточно хорошо поддерживает и лайтово-функциональный стиль и гибридно-процедурный.
А то, что предлагает Unity, это не столько ООП, сколько композитный подход. Идея архитектуры предлагаемой Unity в том, что игра состоит из абстрактный объектов (которые грубо говоря не имеют никаких свойств) к которым приаттачены компоненты, каждый из которых наделяет объект каким-то небольшим (в идеале) свойством.
Но делать игру с глубокой логикой чисто на композиции компонентов без вермишели - требует мастерства. Лично я склонен считать что для сколь-либо сложных игр лучше использовать немного другой подход.

Во-первых, делим всю игру на слой симуляции и слой отображения.

Симуляцию в свою очередь делим на 2 части:
1.1. игровая модель данныех. это набор классов с одними только полями (можно делать только методы, которые не модифицируют состояние класса). Класс "Мир" с полями "монстры", "игровое поле", "очередь хода", "эффекты" и тому подобное. Критерий корректности - сериализация объекта "Мир" дает состояние сейв-файла. Здесь нет ничего об отображении - толкьо суть. Мир здесь описывается так, будто это реальный мир (пусть и со странными дсикретными клеточными правилами), а не чья-то игра.
1.2. поведение. это набор процедур, которые вызываясь каждый кадр пересчитывают состояние мира. Сдвигают летящие стрелы на "speed*Time.delta", декрементят кулдауны, обрабатывают отложенные в очередях события, реагируют на команды ввода (в терминах симуляции это решения принимаемые существами, а не какие-то там команды извне).
1.3. на самом деле, можно использовать классы и в классическом ООП стиле (приватные поля и методы, которые что-то делают умное), но очень важно это делать ТОЛЬКО для универсальных случаев - вектора, матрицы, контейнеры для особого индексирования. и каждый такой класс должен уметь делать только что-то одно. проверочный вопрос - "придется ли этот класс менять, если скопировать в другой проект в другом жанре?", и если ответ "да", то он сделан плохо и лучше расщепить его задачу на предыдущие два пункта.
(в идеале (и для пошага в этом нет проблемы) симуляция ничего не знает о Unity кроме векторной математики)

Отображение в свою очередь разделяется на 3 части: (а вот тут уже Unity полным ходом)
2.1. набор довольно простых и самодостаточных компонентов-MonoBehavior, которые так или иначе позволяют отображать данные из симуляции. Это куклы-марионетки и реквизит (а описанная выше игровая модель данныех - это сюжет спектакля).
2.2. набор процедур, которые выполняются каждый кадр и манипулируют состоянием сцены кукольного театра так, чтобы они соответствовали состоянию симулируемого мира. Это кукловоды.
2.3. обработка пользовательского ввода и передача команд в игровой мир.

На самом деле, то, что я описал отлично ложится на ECS (игровая модель (а в некоторых реализациях и модель сцены) = сущности+компоненты, а процедуры обоих словев - системы), но поначалу от ECS вы ничего не выиграете, только будете отвлекаться и писать лишний бойлерплейт.

А GoF-паттерны во многом устарели. Скорее для общего образования их стоит знать, но не как руководство к действию. Если у вас само по себе что-то получилось похожее на GoF - ок, вы теперь знаете как это называть, но не надо форсировать.

Понижение связности - полезная штука, но не нужно понимать ее как "прячте все за слоями абстракции". В игровой модели вообще на нее стоит забить - там гораздо важнее внятность.
При проектировании игры на чисто-композитном подходе (если игра очень неглубокая) - да, вот там можно и за интерфейсы друг от друга прятать разные компоненты, чтобы было легче сочетать разные компоненты друг с другом. Но в озвученной выше архитектуре компоненты сцены друг о друге знать и не должны - ими рулят процедуры-кукловоды

PS: В пошаговой игре очень значительная часть кода может быть вынесена из симуляции в отображение. Например упомянутый выше полет стрелы - это по большей части отображение. В игровой модели есть только факт выстрела и факт попадания (например, ивент, передаваемый из view в simulation, означающий что можно продолжать симуляцию).

PPS: В терминах упомянутого в треде MVVM, 1.1+1.2 - это Model, 2.1 - View, а 2.2 - ViewModel.

#9
2:47, 24 июня 2022

то непонятно откуда взятые значения переменных

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

#10
11:23, 24 июня 2022

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

#11
11:28, 24 июня 2022

lookid
спасибо!

FourGen
> Вот с этого стоит начать (мое мнение).
> Надо делать как угодно (хотя считаю процедурный подход более масштабируемым)
> Но понимать что откуда берется вы должны в любом случае даже если там полная
> каша.

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

#12
(Правка: 14:05) 13:40, 24 июня 2022

заново заучивать все цепочки

Просто делать надо так, что бы эти цепочки прослеживались адекватно, а вызовы не были напиханы, где не попадя, половина их которых вызывает друг друга через 10 событий, а все эти процедуры/функции раскиданы в 1000 файлах маленького размера, что бы читаемость была получше и которые в свою очередь так же не имеют четкой структуры, прерывая выполнение в любом месте и вызывая не линейно все, а черти знает как. В противном случае да, надо будет пол года сидеть и вспоминать с чего бы это при нажатии туда-то у меня срабытывает вот это, если оно никак не связано и почему.
Есть процедура/функция она выполянет полностью завершенное конкретное действие с объектом или с данными - надо расширить находим - расширяем отлаживаем во всех возможных вариантах и более не трогаем. Вызвать ее может все что угодно, она или сработает для этого или не сработает. Не сработала часть функционала, выполнилась остальная. Понадобилось такое же в другом проекте -> ctrl+c -> ctrl+v и модуль полностью готов к работе без всяких доработок.

Вот надо вращать объект:
Вызвали процедуру вращать объект передали индексы/параметры, там в массив добавился объект, что-то проверилось, начало вращать или не начало, может там должно жать пока объект не будет заданного размера. Вы про это забыли - может делает, не может не делает. Надо перемещать, вызвали другую. Вращать не может, пока объект не того размера, а перемещать может.
Надо отменить вызвали третью. Для каждого объекта есть все все параметры состояния и в любой момент можно проверить, что с ним происходит. В чем тут запутка и что тут впоминать?

Вот вопрос, вы клики обрабатываете как? У вас 1 скрипт для этого? (если нет, то как раз про это и говорю 1 скрипт обработки одних и тех же действий, а далее уже пусть ссылается куда угодно, но по той же логике. Кнопки туда - действия туда-то, еще что-то туда-то убрали скрипт обработки кликов, не вопрос, можно все эмулировать. Нет вообще графики - то же не вопрос все можно эмулировать без нее, логика выполняется, но игнорируется отображение)

#13
11:09, 25 июня 2022

kkolyan хорошо все описал - лайк

ПрограммированиеФорумИгровая логика и ИИ