Comments 96
гораздо более понятно, чем большинство современных систем.
IMO, Finite State Machines — это антипаттерн похуже синглотонов. Он имеет очень узкое применение — анимация и AI, использовать его где либо еще — моветон. За годы работы в геймдеве, я не видел ни одной нормальной реализации FSA которая бы не раздулась в неподдерживоемое спагетти.
Переписать толком можно легко, есть куча паттернов и подходов, начиная классическими вроде MVC / MVVM (которые отлично подходят для UI-driven и 2d игр, с теми или иными изменениями), и заканчиая Entity Component System и Data Oriented Design.
Я считаю, что в таких ситуацях гораздо уместнее писать Service-Centric код и Inversion of Control. Вместо монолитной машины состояний, состояние игры описывается набором сервисов. Каждый сервис имеет свое сосотояние отвечает за что-то одно и только одно, например: анимация, диалоги, инвентарь, квесты, прогресс игрока, сохранения, аутентификация итд.
Сервисы слабо связаны (через интерфейс) и общаются друг с другом по минимуму. Соответственно, ожидания каждого сервиса так же описываются через интерфейс / контракт, например, сервис диалогов может обращаться к сервису интвентаря что бы узнать, имеется ли в наличии предмет, требуемый для выполнения квеста, итд (InventoryService.HasItem(...)).
Логика каждого сервиса может быть протестирована Unit и Integration тестами.
Единая FSA конечно зло, я не смотрел — так ли это в данной игре.
Каждый сервис FSA не сильно отличается от монолитных FSA. Разбивая логику на сервисы, не забывая о принципе единой ответственности и SOLID в целом, вы быстро обнаружите, что FSA вам не нужны.
Приведите конкретный пример функционала и я приведу вам пример как сделать его на обычном OOP, без всяких FSA.
Я видел обратные примеры, когда применение ООП вместо FSA превращало код в адскую лапшу из мутабельных состояний и флагов, когда непонятно, что куда относится и с чем взаимодействует.
let state = ...
switch state {
case state1:
v1.hidden = true
v2.hidden = true
v3.hidden = false
v4.hidden = true
case state2:
v1.hidden = false
v2.hidden = true
v3.hidden = true
v4.hidden = true
...
}
v1.visible = state == state1;
v2.visible = state == state2;
v3.visible = state == state3;
v4.visible = state == state4;
Состояний 300
Я нисколько не против таблиц, кстати они тоже конечный автомат, так что мы отошли от изначального вопроса :) Но если есть еще и какая-то кастомная логика кроме .visible, имхо switch — да, самое топорное, но и самое наглядное/поддерживаемое решение
Даже если претят таблицы, то хотя бы использовать этот факт:
скрывать 3 из них и показывать 4-ю
v1.hidden = v2.hidden = v3.hidden = v4.hidden = true;
let state = ...
switch state {
case state1:
v3.hidden = false;
break;
case state2:
v1.hidden = false;
break;
...
}
Я считаю что вы делаете две потенциальные ошибки.
Во-первых, вы строите всю архитектуру на предположении о том, что у вас может одновременно показываться только одна менюшка и это требование никогда не изменится. Но завтра к вам приходит дизайнер / заказчик и просит сделать drag-and-drop между менюшками, и оказывается что во время перетаскивания надо показать обе одновременно. И в такой момент вы понимаете, что весь код придется для этого выкинуть / переписать с нуля, потому что само понятие машины состояний тут неприменимо (раз может быть несколько состояний одновременно).
Во-вторых, в самом switch-case, конечно, ничего страшного нет, но как только добавится логика, анимации переключения между меню итд, появятся классы типа State, StateTransition и прочих артефактов спагетти-стейт машин, и проблема #1 станет еще хуже.
Как по мне, так хорошо установленные паттерны вроде MVC отлично подойдут здесь. Каждое меню или подменю имеет свою пару Controller — View. Controller отвечает за логику и делает вызовы в сервисы, View — за представление (шаблон меню, анимации). За переключение подменю может отвечать одна функция, которая при разворачивании контент-меню закрывает остальные:
// IMenuController.cs:
interface IMenuController
{
bool IsOpen;
void Open();
void Close();
}
// TopMenuController.cs:
class TopMenuController : IMenuController, INestedMenuContainer
{
private IMenuController[] _submenus;
public void OpenNestedMenu(int index)
{
for (int i=0; i<_submenus.Length; i++)
{
IMenuController submenu = _submenus[i];
if(i == index && !submenu.IsOpen)
{
submenu.Open();
}
else if(submenu.IsOpen)
{
submenu.Close();
}
}
}
// ...
}
Это просто, понятно, читабельно, а главное, требование "одновременно может быть показано толко одно меню", запрограммировано ровно в одной функции на 12 строк кода и может быть в любой момент времени изменено.
Не стройте архитектуру на предположениях, которые могут оказаться неверны.
Бинд обработчик(и) — состояние лежит во внешнем файле и в любой момент может быть откорректирован, требование ТЗ.
Для удаления с шины есть отдельный обработчик, который мониторит неиспользуемые состояния и состояние окончания обработки.
И как здесь без конечных автоматов?
использовать его где либо еще — моветон
А парсеры?
Ну, я говорил об играх. Понятное дело, за пределами игр есть ряд узкоспециализированных задач и алгоритмов где FSA применимы. Но для геймплей логики их использовать — это зло.
Я думаю там уже потирают руки и готовят разбор…
Если вам интересно посмотреть, какие ошибки мы там нашли, предлагаю почитать статью:
VVVVVV??? VVVVVV!!!
Работает, не трожь
Да. Судя по разному форматированию, это не невнимательная копипаста. Возможно, логика работы программы описывается именно так. Блок с первым условием — валидация/инциализация входных данных. Второй блок — обработка данных. Условия сейчас одинаковые, да.
Хотя понятно, конечно, что C-код игры вторичен, вначале был flash, а в Action/Ecma/JavaScript с enum как-то не особо хорошо.
Так если каждое из них имеет какой-то смысл в механике процесса, почему бы и не быть этим самым "вменяемым именам"? Или речь о том, что имена, отражающие суть, быстро разрастутся до нечитабельной длины?
А что такого?
https://github.com/php/php-src/blob/9d7e03c325473024e54c864f0379efc1bbf03e72/Zend/zend_vm_opcodes.h#L79
Тут не 300, конечно, только 200, но сути не меняет.
В Godot ещё лучше реализовано. Но в целом да, игры — они про ограничение свобод игрока (правилами мира, сюжета и т.п.), а прикладное программирование — про расширение свобод, т.к. это инструменты для пользователя, которыми он может решать задачи, о которых разработчики даже не задумывались. Поэтому подходы действительно очень разные.
если в игре VVVVVV код на 309 элементов, сколько их может быть в игре AaaaaAAaaaAAAaaAAAAaAAAAA!!! (A Reckless Disregard for Gravity) ?
Ну вообще все говнокодят, просто разборы говнокода в играх как правило интересны, потому связаны с какой-то фичей в игре. А разбор говнокода в условном notepad не интересен. Вот и вся разница в говнокоде в геймдеве и не в гемйдеве.
Кстати говоря, о багах в Блокноте — это тоже может быть интересно. Например, если реверсить код https://habr.com/ru/post/264081/
--то купите экран побольше!
Соотаетственно в начале каждого экрана писалось, что эта часть исходников делала (во многих системах каждому экрану с исходниками соответствовал экран с документацией. Тоже 1 КБ), и загрузка и редактирование происходило поэкранно. Можно было выводить список первых строк экранов-блоков.
По историческим причинам блочные редакторы всё ещё являются частью некоторых достаточно современных форт-систем. Где под эти самые блоки отводится просто отдельный текстовый (ну, почти) файл. Например см. www.forth.com/starting-forth/3-forth-editor-blocks-buffer (там код адаптированный к коммерческому и ещё, вроде, живому SwiftForth).
Копчик — да, уязвимая штука — легко крашится и потом вся система страдает.
Но мне кажется рефакторить нужно сначала наиболее критические места, ИМХО:
1) дублировать/резервировать мышцу для поддержания давления в кровеносной системе.
2) усилить/укрепить систему монтажа «ЦП» и большинства сенсоров.
3) разнести в пространстве интерфейсы систем отвода отработки и репродуктивной системы. (ибо не гигиенично).
:)
Не пытайтесь переплюнуть Milfgard. Он описал гораздо больше багов.
Ну а что писать? Конечно игрушку какую-нибудь. Что-нибудь простое и из времен молодости.
За два дня(в буквальном смысле за 2 дня) наклепал говнокода и как водится — «х.як-х.як и в продакшин» (в смысле в магазин приложений). Не запаривался никакими игровыми фреймворками, все было сделано на обычном виндовом Canvas'е, без «эффектов» — просто игровые юниты + гравитация.
Потом несколько раз обновлял приложение (добавлял новые уровни, предварительно их пройдя локально у себя на телефоне + мелкие доработки, типа вкл/выкл звук и еще что-то там), потом наигрался и забыл/забил.
Так вот, к чему это я все? К тому, что код у меня в этой игрулине был примерно такого же уровня — он абсолютно не был продуман, т.к. изначально вообще не было никакого плана разработки. Ни количества уровней, ни списка фич, а общая концепция сводилась к «ща наклепаем и поглядим, как оно будет, а когда-нибудь потом сделаю как надо».
Скорее всего автор упомянутой игры пошёл по такому же пути — слепил в коде какую-то мелькнувшую идею, по ходу разработки обвесил ее всякими плюхами и дополнениями, ну а после просто поленился делать рефакторинг (хотя судя по коду, там проще с нуля переписать, чем пытаться привести все это хозяйство к «правильному» виду).
Мир — довольно хаотичен, сложен и переплетен. Окинуть разумом его очень сложно, поэтому, мне кажется, что идеальное ТЗ на что-то сложнее калькулятора — заведомо невозможно. Разработка по принципу добавления кирпичиков, чтобы каждый час труда развивал продукт — невозможна. Ну или раз в тысячу лет рождаются титаны мысли, способные на это. Реальный путь — это осознание неизбежности ошибок как в коде, так и в дизайне. Делаем тяп-ляп, запускаем, видим где криво — переписываем. Потом доводим код до состояния невозможности чтения-понимания (зато — к этому мнению в головах есть понимание задачки «как надо») и уже делаем новую версию на чистовую.
Некрасиво? Да. Но эта рабочая схема. Весь мир создан на скотче и синей изоленте. Идея «давайте просто хорошо придумаем как надо и сделаем» — только в голове у юношей бывает, до первого раза, когда они все делали как надо, сами, и выясняется, что когда «придумывал как надо» — придумал фигню какую-то.
А все потому, что мне пришло в голову походить хвостом за инженерами (для которых это ПО разрабатывалось), понаблюдать процесс работы, поспрашивать «а чо это вы тут делаете такое, ааа?».
В результате чего уже на второй день стало понятно, что в ТЗ (54 страницы!) описана какая-то лютая херня, к реальной необходимости имеющая довольно посредственное отношение, проще nanoCAD поставить и обучить персонал, чем городить написанное.
На выходе получился чудовищный гибрид Paint'a с VIM'ом, но оно работало именно так, как было нужно людям, непосредственно работающим с этим ПО — минимум движений, максимум результата.
Да, говна и палок тоже порядочно было, т.к. многое приходилось придумывать и прилеплять по ходу разработки. Отрефакторил конечно самые «ужасные» куски перед продакшином, но! Все это работало (и работает по сей день) как часы, и послушно, как гимназистка. Хотя до «идеального кода» все еще очень далеко, но лично мне важнее именно «шоб работало как надо», а не что-то другое.
Если кратко это резюмировать: не нужно жить в говне, если говно случилось, нужно вытереть, но пытаться в коде совместить впечатление от Лувра и стерильной лаборатории одновременно никакого смысла нет.
У меня была другая ситуация. В начале своего пути становления как программиста я много говнокодил и захлебывался от предварительных оптимизаций, отсутствия системы. В результате не доводил проект не до что до конца, даже до середины. Даже начинал задумываться а мое ли это?
Сейчас процент более менее завершенных проектов куда больше :)
Статья про одну из игр с ужаснейшим кодом:
https://nooby-games.ru/%D1%89%D0%B8-%D1%81%D0%B8%D0%BC%D1%83%D0%BB%D1%8F%D1%82%D0%BE%D1%80-%D0%B6%D0%B5%D1%81%D1%82%D0%BE%D0%BA%D0%BE%D1%81%D1%82%D0%B8/
Плюс можно делать всякие аддоны, тулзы и прочая, но их, как я понял, нельзя будет монетизировать.
В остальном да, это весьма ограничивающая лицензия.
Тут уже несколько раз приводился switch на 300 состояний. Попробуйте вспомнить хоть какую-нибудь сущность, представителей которой вы можете описать в количестве 300 штук. Я почти уверен, что в оригинальном коде игры этому состоянию нет названия, потому что автор не попытался его как-то назвать. А значит и не было попытки как-то его декомпозировать.
а люди еще удивляются, посему у них на свежевышедших играх сейвы слетают, а патчи могут весить больше, чем сама игра.
Апдейт большого размера уже редкость, но вот игры которые перевалили за 80 гигов на харде уже не редкость. И та ки да, 99 процентов файлов из этих 80 гигов это медиа файлы, как картинки с разным разрешением и музыка. Если повезет то музыка в МП3 и можно скопировать на телефончик. Если не повезет, то нужно понять как конвертировать один тип файла в другой.
Но ёлки-палки, внутри довольно ужасный код на голом С. Многие вещи, которые я заложил в самом начале, так и остаются в неизменном виде. С тех пор плагин стал делать гораздо больше функций и это тоже добавило некоторого хаоса. Я это потихоньку привожу в порядок, но на самом деле проще всё сжечь и переписать заново.
Это я всё к чему. Если бы я тогда начал продумывать архитектуру, то эта штука скорее всего вообще бы не появилась. Я бы просто утонул в деталях, в начале я вообще не представлял за какую сложную штуку взялся и сколько там нюансов.
Ладно игры — работал в одной Fin-Tech конторе, которая делала софт для top-tier investment банков: там был тоже был один мега-фактори с огромным case-switch'eм. В какой-то момент он превысил 65536 строчек кода, и начались проблемы с дебагом в VisualStudio
У меня есть живой пример — люди в одном проектном институте копипастили код АСУТП, что он перестал влезать в контроллер по памяти (100500 задвижек или что то типа).
Ну я по молодости, примерно так нетолерантно им и ответил
На работе в энтерпрайзе я так не пишу, потому что этот код нужно будет поддерживать и переиспользовать. В играх же зачастую какой-то код весьма специфический, и выполняется один раз за игру — так зачем из него делать фасад фабрик?
Наверное, есть исключения — или долгоиграющие AAA проекты (кхе-кхе, на тебя смотрю, Star Citizen), или всякие MMO/MOBA у которых очень долгий цикл сопровождения. Но да, для проектов поменьше, для инди, выкатить рабочее важнее внутренних красот.
Хотя класс гранатомёта, это же довольно простой объект, произошёл выстрел, и в методе где запускается пуля, выпускается граната по направлению выстрела. Вроде всё просто, метод FireTrace сделал виртуальным и переопределил в гранатомётах.
Я из-за этой ошибки, не смог написать скрипт ПЗРК на Lua для оригинального движка. Логика ПЗРК на порядок сложней чем в простом РПГ.
ЗЫ
В XRay граната-ракета это такой же наследник CGameObject, как и все остальные объекты в игре. В других движках, может по другому реализовано, в точности наследник пули, как CRYENGINE.
CWeaponRG6 может стрелять гранатой когда магазин пуст при определённых условиях
Как-то во время боя замолчал пулемет.
— Ложкарёв, почему прекратили огонь?
— Товарищ командир, патроны кончились!
— Лошкарев, ты же коммунист!
И пулемет застрочил с удвоенной силой.
И вообще, раз пошла такая пьянка...
ЗЫ
Как видно РГ-6 оружие читерское, особенно в мультиплейере! Как разрабы поступили в этом случае? Очень просто, они в МП заблокировали разрядку оружия во обще! =) Вот так просто, контекстное меню заблокировали, и всего делов. Хотя починить гранатомёты не сложно, но вероятно свободных программистов для этого не было. С этим связано не справедливая система оплаты в GSC. Надеясь сейчас это не так, а то в СТАЛКЕР-2 таких багов будет не меньше.
Публикация кода VVVVVV показала, насколько грубо устроены игры внутри