На прошлой неделе создатель Dicey Dungeons Терри Кавана отпраздновал десятую годовщину своей давней игры VVVVVV, by опубликовав её исходный код [перевод на Хабре]. Если объяснять просто, то это значит, что любой человек может теперь посмотреть, как создавалась игра, потому что каждую строку кода можно внимательно изучить.
Такое нечасто случается и поэтому ценность публикации этой информации очень велика. Люди могут учиться по ней или улучшать код. Некоторые отзывы на исходный код VVVVVV были ужасными — исследователи увидели вещи, которые можно было написать лучше. Возможно, Кавана предвидел это — в своём посте, где он объявил о публикации кода, Терри признаёт, что «технически игра VVVVVV не очень хорошо продумана! Даже по стандартам инди-разработчиков-самоучек, код довольно хаотичен».
Существует заблуждение, что написание кода само по себе является изящным и продуманным, ведь в конечном итоге, это своего рода написание логики, не так ли? Не зря ведь это называется компьютерными НАУКАМИ? Но в реальности всё гораздо сложнее. Очень часто истории разработки видеоигр показывают, что поскольку в играх есть так много элементов, от геймдизайна до звука, что часто собираются в единое целое только в последний момент, если вообще когда-нибудь собираются.
«Существует название для игр, в которых код едва связан, имеет глупую архитектуру, в которых ошибки почти невозможно исправить, а костыли громоздятся на велосипеды. Они называются „выпущенные игры“».
«Почти каждая игра, над которой я работал, выпускалась именно в тот момент, когда навоз и палки, удерживающие всё здание от разрушения, находились на грани катастрофы», — написал разработчик Джеймс Паттон в обсуждении кода игры в Twitter.
Почти каждый разработчик, с которым я общался, говорит то же самое.
«Игры — это не обычное ПО, а сложный механизм, для успешного издания которого требуется знание множества дисциплин, и часто для того, чтобы уложиться в срок, приходится идти на жертвы», — написал в письме разработчик игр Джеймс Симпсон. «Я знаю многих разработчиков, из кожи вон лезущих, чтобы сделать свой код идеальным. Этот пример открытого исходного кода показывает, что можно успешно выпустить игру, не достигая такого уровня совершенства».
В случае VVVVVV обширные дискусии вызвал отдельный фрагмент исходного кода, имеющий очень хаотичную структуру. По сути, он помогает определить, в каком состоянии находится игра: например, в катсцене или части диалога. В обычной ситуации многие из этих состояний группируются по отдельности — код мини-игр пишется отдельно от, допустим, механики прыжков — но не в случае этой игры. Например, в коде Кавана состояния игры, связанные с катсценами, разбросаны по разным частям программы и перемешаны с такими элементами, как игровые режимы и главное меню.
Сегодня видеоигра «VVVVVV» перешла в #OpenSource и кто-то обнаружил в коде конструкцию из нескольких сотен switch. #programming Это одновременно прекрасно и отвратительно.
Зак Гейдж, работавший над такими мобильными играми, как Pocket-Run Pool и Really Bad Chess, разобрал для нас эту печально известную конструкцию из switch, сказав, что «это прекрасный пример того, о чём Терри скорее всего не знал, когда начинал писать игру: сколько катсцен в ней будет, как будет работать главное меню, и сколько странных потенциальных конечных состояний странных режимов может возникнуть».
Если бы Кавана стремился сделать код безукоризненным, то он мог бы прекратить вносить такие изменения и лучше упорядочить код. Но вместо этого он решил двигаться дальше и сделать то, что получится, заставив одну гигантскую часть кода управлять сотнями различных вариантов.
«Вместо того, чтобы сделать шаг назад и упорядочить всё, Терри наверно подумал что-то типа „Да, я собираюсь просто добавлять состояния в огромный оператор switch каждый раз, когда мне понадобится что-то новое“. Это на 100% нормально и по сути необходимо для выпуска игры».
Гейджу очень близко известен такой образ мышления — он рассказал нам, что оглядываясь на разработку игры Ridiculous Fishing, «в буквальном смысле не понимает, как коду удаётся работать, ведь он так плохо написан». Даже другие члены команды не полностью понимают, как работает их игра.
Но знаете что? Ridiculous Fishing всё равно получила несколько наград и заработала на несколько месяцев почти миллион долларов. Очевидно, что разработчики должны устранять ломающие игру баги и всё, что может встать на пути игрока к наслаждению игрой, но, как говорится, лучшее — враг хорошего.
«Всегда будут появляться незапланированные части, и если вернутся назад для упорядочивания кода, то это означает, что ты потеряешь время, которое можно потратить на написание нового кода, добавление новых функций или рисование арта», — говорит Гейдж.
Но несмотря на весь снобизм, вызванный в социальных сетях игрой VVVVVV, Кавана, похоже, не теряет чувства юмора и шутит над всем этим.
Каждый увиденный мной скриншот ужасных недостатков в исходном коде VVVVVV лишь делает меня сильнее.
«Не знаю, что мне сказать по этому поводу?», — написал он в посте о публикации исходного кода. «Я был молод, и мне было интереснее создать что-нибудь на экране, чем реализовать это правильно. Вероятно, самое лучшее в исходном коде VVVVVV — он стал доказательством того, что можно слепить что-нибудь самостоятельно, даже если вы не очень хороший программист».
О том, как разработчики игр решают проблемы при помощи быстрых хаков, можно прочитать в серии переводов «Грязные трюки в коде видеоигр»:
Грязные трюки в коде игр
Разработчики о самых грязных программных трюках в играх
Грязные трюки и оперативка
Грязные трюки разработчиков видеоигр
neochapay
github.com/TerryCavanagh/VVVVVV/blob/master/desktop_version/src/Game.cpp#L622 тот самый switch из 309 элементов.
inv2004
гораздо более понятно, чем большинство современных систем.
Siemargl
Обычная стейт машина FSA, да и иначе толком не перепишешь.
splatt
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 тестами.
Siemargl
Каждый сервис — своя FSA, остальное — модное словоблудие.
Единая FSA конечно зло, я не смотрел — так ли это в данной игре.
splatt
Каждый сервис FSA не сильно отличается от монолитных FSA. Разбивая логику на сервисы, не забывая о принципе единой ответственности и SOLID в целом, вы быстро обнаружите, что FSA вам не нужны.
Приведите конкретный пример функционала и я приведу вам пример как сделать его на обычном OOP, без всяких FSA.
TargetSan
Я видел обратные примеры, когда применение ООП вместо FSA превращало код в адскую лапшу из мутабельных состояний и флагов, когда непонятно, что куда относится и с чем взаимодействует.
iago
Есть меню вверху экрана, там в зависимости от нескольких флагов/условий могут появляться 4 одинаковых по размеру контент-менюшки. Нужно по совокупности условий скрывать 3 из них и показывать 4-ю. Как вы понятно это сделаете, если не, скажем:
unC0Rr
Так короче и меньше шансов ошибиться, хотя по понятности всё равно не лучший вариант:
agarus
Состояний 300
iig
Кодировать состояния в таблицу, таблицу парсить либо в compile time (генератор кода), либо в runtime.
iago
а если не только прятать/показывать надо? Конкретно в этом примере мне надо чтобы на вьюхе №1 показывалась определенная анимация, если это переход с 4 стейта в первый, и т.п., и логики там еще пару строк в каждом кейсе есть.
Я нисколько не против таблиц, кстати они тоже конечный автомат, так что мы отошли от изначального вопроса :) Но если есть еще и какая-то кастомная логика кроме .visible, имхо switch — да, самое топорное, но и самое наглядное/поддерживаемое решение
agarus
Хештабшицей статус -> маска, например. Или массивом массок если state это числа в разумных пределах.
Bronx
Даже если претят таблицы, то хотя бы использовать этот факт:
iago
можно было бы и так для красоты и краткости, так иногда и делаю, а что если на какие-то вьюшки мне нужно показывать с анимацией, если переход от четвертой к первой, и только этот переход? Да, такая логика у приложения, ничего не поделаешь. И имхо чем кастомней код тем больше ему нужно быть кодом, таблицами все не закодируешь. А старый добрый конечный автомат всегда выручает
splatt
Я считаю что вы делаете две потенциальные ошибки.
Во-первых, вы строите всю архитектуру на предположении о том, что у вас может одновременно показываться только одна менюшка и это требование никогда не изменится. Но завтра к вам приходит дизайнер / заказчик и просит сделать drag-and-drop между менюшками, и оказывается что во время перетаскивания надо показать обе одновременно. И в такой момент вы понимаете, что весь код придется для этого выкинуть / переписать с нуля, потому что само понятие машины состояний тут неприменимо (раз может быть несколько состояний одновременно).
Во-вторых, в самом switch-case, конечно, ничего страшного нет, но как только добавится логика, анимации переключения между меню итд, появятся классы типа State, StateTransition и прочих артефактов спагетти-стейт машин, и проблема #1 станет еще хуже.
Как по мне, так хорошо установленные паттерны вроде MVC отлично подойдут здесь. Каждое меню или подменю имеет свою пару Controller — View. Controller отвечает за логику и делает вызовы в сервисы, View — за представление (шаблон меню, анимации). За переключение подменю может отвечать одна функция, которая при разворачивании контент-меню закрывает остальные:
Это просто, понятно, читабельно, а главное, требование "одновременно может быть показано толко одно меню", запрограммировано ровно в одной функции на 12 строк кода и может быть в любой момент времени изменено.
Не стройте архитектуру на предположениях, которые могут оказаться неверны.
DrunkBear
Допустим, есть шина (rabbitMQ), на которую льются объекты и подписаны обработчики. Каждый обработчик берёт объект со своим state, что-то делает, меняет состояние и выкидывает обратно в шину.
Бинд обработчик(и) — состояние лежит во внешнем файле и в любой момент может быть откорректирован, требование ТЗ.
Для удаления с шины есть отдельный обработчик, который мониторит неиспользуемые состояния и состояние окончания обработки.
И как здесь без конечных автоматов?
namikiri
А парсеры?
splatt
Ну, я говорил об играх. Понятное дело, за пределами игр есть ряд узкоспециализированных задач и алгоритмов где FSA применимы. Но для геймплей логики их использовать — это зло.
Static_electro
У меня обратный опыт. Тем не менее, я согласен с тезисом про спагетти — стейт машины надо проектировать осторожно и внимательно. Зато когда она работает — то она работает хорошо.
0xd34df00d
Явные — плохо. PEG получше.
0xd34df00d
Бустовские стейтмашины смотрят на этот тезис с недоверием.
MaxVetrov
Немного выше зачем-то 2 if-a сделали:
Работает, не трожь )namikiri
PVS-Studio на них нет!
MaxVetrov
PVS-Studio в данном случае, возможно, выдаст v525.
Cenzo
Я думаю там уже потирают руки и готовят разбор…
GGribkov
Да, готовят! Сегодня выйдет статья :)
GGribkov
PVS-Studio на них есть :) Мимо такой игры мы пройти не смогли.
Если вам интересно посмотреть, какие ошибки мы там нашли, предлагаю почитать статью:
VVVVVV??? VVVVVV!!!
iig
Да. Судя по разному форматированию, это не невнимательная копипаста. Возможно, логика работы программы описывается именно так. Блок с первым условием — валидация/инциализация входных данных. Второй блок — обработка данных. Условия сейчас одинаковые, да.
panteleymonov
Тут еще возможно, что во время отладки меняли значение statedelay=0 на что то еще, что меняло логику. Или меняли местами statedelay--.
kotlomoy
enum? не, не слышал
Siemargl
enum в С не спасает, контроль появился в С++
rkfg
А в игре как раз C++
kotlomoy
Я всего лишь сказал, что
читается лучше, чем Вы так не считаете?bentall
А за что минус-то «предыдущему оратору»? Перечислить состояния машины состояний в enum'e и делать switch не по magic numbers, а по осмысленным идентификаторам — самый простой и естественный шаг. Который улучшил бы читаемость кода и мало-мальски облегчил бы его поддержку и отладку даже автору.
Хотя понятно, конечно, что C-код игры вторичен, вначале был flash, а в Action/Ecma/JavaScript с enum как-то не особо хорошо.
le1ic
300 enum значений? С вменяемыми именами?
Cerberuser
Так если каждое из них имеет какой-то смысл в механике процесса, почему бы и не быть этим самым "вменяемым именам"? Или речь о том, что имена, отражающие суть, быстро разрастутся до нечитабельной длины?
Kolobok86
В redux так и живём, по сути, и ничего, норм ))
wxmaper
А что такого?
https://github.com/php/php-src/blob/9d7e03c325473024e54c864f0379efc1bbf03e72/Zend/zend_vm_opcodes.h#L79
Тут не 300, конечно, только 200, но сути не меняет.
Keynessian
И на что его критики предлагают заменить?
faoriu
Нуу можно сделать массив или хэш из классов или просто лямбд, например.
iago
будет тот же state machine, только на лямбдах, не?
snuk182
В
джавенекритичных к производительности приложениях это паттерн Стратегия. Почти уверен, что в играх он неуместен, но настаивать не буду.IvanBulb
В играх очень сложно и не нужно разделять все на MVC паттерн — там другая парадигма программирования))) Парадигма которая очень неплохо реализована в Unity3D)))
Из за этого именно такой код и будет в 90% игр)))
rkfg
В Godot ещё лучше реализовано. Но в целом да, игры — они про ограничение свобод игрока (правилами мира, сюжета и т.п.), а прикладное программирование — про расширение свобод, т.к. это инструменты для пользователя, которыми он может решать задачи, о которых разработчики даже не задумывались. Поэтому подходы действительно очень разные.
ApeCoder
В приткладном — свой мир со своими бизнес правилами. Пользователю нельзя давать выстрелить себе в ногу. И не только себе.
v1000
если в игре VVVVVV код на 309 элементов, сколько их может быть в игре AaaaaAAaaaAAAaaAAAAaAAAAA!!! (A Reckless Disregard for Gravity) ?