С Днем Программиста
Ну вот и наступил самый круглый день в году.
Поздравляю всех причастных к программированию, начиная от микроконтроллеров и заканчивая скриптами на веб-мордах сайтов.
Поздравляю всех кто зарабатывает себе на жизнь этим не сильно легким трудом и желаю как можно более вменяемых заказчиков и поменьше багов. (хотя конечно хорошо задокументированный баг — не баг, а фича).
С Праздником!
122 комментария
Но с другой стороны спорить тоже сложно, ибо мозгоебли с питоном куда меньше чем с/с++
P.S. И с пятницей 13-го!
P.P.S. И сегодня был нормальный день без неудач.
Традиционный баян. ;)
А профессионалов — с праздником.)
А то мгновенно все табуном побегут со всякой шляпой и криками «тыжпрограмист, почини мне мангал сломавшийся!»
Это Луна — крутой программист, но скрывает это (по вышеописанной причине).
Я сломаю последовательность — программист, но анимэ практически не смотрю.
Спойлер
Присмотрелся к коду — ой мамо, роди меня таки обратно! Ты просто мастер копипаст 99лвл!.. какая жеееееесть… так даже на бейсике для спектрума 30 лет назад писать считалось дурным тоном…
Можно прямо в коде, конечно, но рано или поздно ты придешь к тому же, к чему и я — использование внешнего скриптового языка (стандартного или нестандартного) плюс собственный дата-ориентированный язык. Вшивать логику в код ядра — плохая практика.
ОК, возможно, в таком случае вынос логики во внешние скрипты и не обязателен.
Зачем переусложнять проект? Зачем добавлять лишнюю прослойку, в которой могут быть баги? Что это даст на текущем этапе?
Как говаривал Кнут: преждевременная оптимизация — корень всех зол.
Вынесение игровой логики из ядра во внешние файлы — именно такой паттерн.
Ядро содержит сложные и сильно связанные фрагменты кода — рендер, обсчет физики, обновление игрового мира — если есть, то и сетевое взаимодействие.
Игровая логика содержит простые и слабо связанные фрагменты кода — параметры юнитов, действия заклинаний, какие пони производятся в облачных казармах, сколько Трикси Луламун нужно для победы над Урсой Старшим и подобное.
Их разделение — логично.
Уменьшение горизонтальных связей. Если вся система написана на C++, то все связано — каждый include тянет за собой свои include, и в один момент, добавив Твайлайт крылья, ты обнаружишь, что все прочие единороги потеряли рога.
Если же логика крылатой Твайлайт находится во внешнем модуле на скриптовом языке — ты знаешь, что метод setWings может быть вызван только из внешнего скрипта, а не откуда-то еще — и вероятность обрушить проект внесением пустякового изменения в логику отдельной едницы уменьшается.
Здесь же таким разделением — ну, хорошо, вы обезопасили себя и добились того что упали только внешние скрипты и только у созданных в них единорогов поотваливались рога, а ядро системы стоит. Только зачем? В чём его самоценность? Я бы понял, если бы речь шла о модульной системе, у которой есть значимое ядро, которое должно работать при любых условиях — скажем, при проектировании космического корабля, вне зависимости от того, что за дичь будет творится в системе мигания левой лампочкой.
Но блин, это не та программа! Если у вас есть баг — то не надо его прятать, лучше его обнаружить как можно раньше. Зачем тут пользователю возможность продолжать играть в сломанную игру?
Не отвалятся. Потому что внешние скрипты связаны с ядром через жестко заданные процедуры. И если мы пишем в скриптах
twilight.addWings(2);
то мы уверены, что данный вызов не сделает ничего, кроме как добавление крыльев. Не увеличит размер исходного кода ядра, не сломает стек, не кинет нам SIGSEGV и прочую прелесть.
Почему то же самое нельзя написать, создав модуль на C++, включив в него twilight.h и в процедуре initMap() сделав тот же вызов?
Можно. Но опыт тысяч коммерческих и миллионов любительских проектов говорит — ядро отдельно, скрипты отдельно.
PS: Еще аргумент — кода пишешь логику игры в ядре, довольно быстро начинаешь городить огород из вызовов того, чего вызывать не нужно. Скажем, хочется добавить крылья только тогда, когда у Твайлайт есть 6 друзей. И понеслась
int cnt=0;
foreach(QPony pony: map.getponies()) {
if (pony.isFriendFor(twily)) cnt++;
}
if (cnt==6) twily.addWings();
и в итоге код становится мусоркой.
А если мы сразу знаем, что ни один код нельзя вызвать иначе, чем из внешнего скрипта — то сразу создаем функцию
int getFriendsCount(QPony pony)
и больше никогда уже не лезем в ядро для такой проверки.
Вынос логики в скрипты заставляет структурировать код.
PSPS: И последнее — вынос логики в скрипты позволит расширить игру тем, кто не может править ядро. Не только лишь все могут освоить C++ (я не смог), а вот простенькие скрипты на Lua или JS — большинство.
Уверены ли? Почему этот код не может сломать стек, кинуть SIGSEGV и прочую прелесть?
Знаете ли вы, почему на Табуне нельзя сменить аватарки? Потому что одно время табун можно было положить картинкой. Картинкой, Карл! Интерпретатор картинок(да, такие тоже есть) рассчитывал гигантский размер при попытке распаковки маленького файлика и отжирал всю память на сервере. Потому и отключили загрузку намертво; ну а почему не включили после фикса — это уже совсем другая история.
Так что уверенность что вызов не сломает ничего — ну, она несколько странная. Если всё написано без уязвимостей и идеально, то да, иначе возможны варианты. Но писать подобный интерпретатор — уже задача по сложности большая, чем вся эта карточная игра.
Миллионы мух не могут ошибаться?
Это очень, очень плохая практика — использование средств без понимания того, зачем они нужны, их преимуществ, недостатков и границ применения.
А в следующей функции захочется проверить, друзья ли Твайлайт и Берри Панч. И добавим функцию для определения дружбы. А в следующей — что все друзья Твайлайт старше 18 лет. И добавим функцию для проверки возраста друзей. А в следующей…
Код сам по себе не становится мусоркой. То, что вы показали — в самом по себе этом коде нет ничего плохого. Да, можно вынести в функцию общую часть, но в том-то и ключевое слово — общую. Если это единственное место в программе, где проверяется число друзей Твай — то что нам даст функция, кроме лишнего слоя абстракции, замедления выполнения, и неправильного разделения ответственности? То, как должна выглядеть функция в интерпретаторе — doForEachFriends(pony, lambda), GetFriendsCount(pony) или TwilightHaveExactSixFriends() — это очень большой вопрос, ответ на который не всегда известен на раннем этапе; пытаться же ограничить список функций до того как будет ясно, чем именно занимается логика — ну, это приводит к плохой архитектуре и расхлёбыванию проблем.
P.S. Когда-то давно, когда я учился ещё на первом курсе, на самой первой паре нам дали простенькую задачку. Надо было написать свой собственный интерпретатор своего собственного языка, и составить простые программки — чтобы считало факториал и числа фиббоначи. Были и те, кто старался выполнить это задание, построив интерпретатор с двумя функциями — fibbonachi(x) и factorial(x). Как думаете, хороший ли это подход?
У Саши есть помощник, который может освоить lua, но не освоил питон? А кто это?
Ещё раз, практическая применимость в данной конкретной ситуации. Пока что я вижу только желание бессмысленно раздувать код «чтоб всё было как у всех» и наезды, что человек это не делает. Это действие очевидно даёт лишнюю работу и снижение качества продукта(ибо увеличение кодовой базы, в которой можно наделать багов). И вы это обосновываете нуждой в фантомном помощнике?
ru.wikipedia.org/wiki/SCUMM
С 1987 года, обратите внимание. Скажите тем ребятам, что они усложняли архитектуру, и нужно было делать на C++ все их квесты.
Это делается не с целью упрощения архитектуры, не с целью разграничения кода, а с целью привлечения новых людей к разработке. Если есть человек в команде разработки, который знает бейсик, но не знает и не хочет знать питон — можно и интерпретатор бейсика встроить в игру. А если такого человека нет, то зачем это делать? Встраивать на случай вдруг появится, и переносить всю логику на бейсик?
Исходный код на 200К строк всегда имеет более сложную архитектуру, чем исходный код на 100К. Просто потому, что как ни извращайся — а всё равно связи потянутся между.
Плюс, языки низкого уровня — они адски сложны и опасны. Даже вызов pony[i]->doIgogo() может дать шесть-семь вариантов проблемы, начиная от неправильного индекса массива и заканчивая нереализованным методом doIgogo
Потому на низком уровне пишем критичные вещи вроде рендера и сетевых протоколов, а на всяких скриптах (стандартных или кустарных) — уже логику карт, сценариев и юнитов.
Но ведь добавление интерпретатора и вынос логики на другой язык — это и есть увеличение количества строк в два раза и связей. Зачем вы это делаете, мистер Андерсон? Зачем?
А где в данном конкретном примере вы нашли язык низкого уровня? У нас тут вполне себе скриптовый питон, просто напоминаю. Нет, конечно, варианты с неправильным индексом и методом всё ещё актуальны, как, впрочем, и практически для любого языка — но они не отстрелят вам ногу.
Нет.
Код конкретной карты, вынесенный во внешний интерпретатор — полностью изолирован от других карт. Он может посыпаться, если ошибка в ядре, но не если ошибка в другой карте.
Да.
Эммм… а что это даёт?
Другая карта с ошибкой по-прежнему имеет возможность насовать всем пони по четыре пары крыльев и рог в задницу. Но ура — код этой карты не закрашит систему, просто охренеет от такого оборота событий и не сработает. Или сработает. Или сработает, но не так. А что это даст?
Ещё раз: самоценность ядра тут нулевая. Самоценность изолированных, одиночных карт — тоже. Это не браузер, где каждая вкладка должна быть изолирована от всех остальных и работать даже если другая загнулась. Зачем тут это?
Да ну? То есть, когда ты делаешь игру на 100 карт, и внес поправку в одну карту — тебе не хочется, чтобы остальные 99 карт не поломались?
Это какой-то сложный юмор, мне непонятный.
У меня в игре 50 сценариев, и я уверен, что если я внес новый эффект в одну карту — другие не сломаются, а будут работать так же.
С ядром так не получится — там если одну строку изменил, нужно тестировать все функции, полностью.
Также, если у вас не было бы изолированности между сценариями, если вы создали на первом предмет, ломающий стены, и его можно было бы пронести вместе с героем через кампанию на третий уровень — у вас будут точно такие же проблемы, и вам точно так же пришлось бы тестировать полностью. Изоляция компонентов и вынос в скрипты — это две разных вещи.
И внешнее скриптование эту абстракцию может сделать проще и понятней. Не сможешь ты из скрипта обработки карты вызвать код другой карты — только обратится к менеджеру карт.
А почему не можешь?
Это вы накладываете существенное ограничение на игровую логику. Нельзя получается, скажем, сделать карту «повторно активирует „стук копыт“ соседних карт», или просто «копирует свойства». Базовые механики перестают работать! Если вы делаете интерфейс, что огранчивает дизайнеров потому что программистам так удобнее и хочется — ну, далеко вы не уедете. Я уж молчу о том что у вас регулярно игровая логика, за вынос которой вы так топили, в ядро назад уезжает.
У других может быть иное мнение.
Это базовая вещь для создания любого игрового проекта с наличием сколько-нибудь сложного поведения.
Вынос в скрипты это так-то не оптимизация, наоборот на взаимодействие основного кода и скриптов тратятся память и скорость, но это перекрывается удобствами разработки, например возможность вносить правки в геймдизайн без перекомпиляции кода. Решил например геймдизайнер, что файрбол должен не 20 а 25хп снимать, что проще — ему поправить одну строчку в spells.lua, или идти к программистам, чтобы они перепахали и перекомпилировали исходник на С++? Да и геймдизайнеру намного проще разобраться с каким-нибуть LUA или питоном, чем с хардкорными С/С++/Java. Так же такое разделение позволяет вообще при необходимости перепахать/поменять движок как угодно, почти не затрагивая игровую логику. Ну и так далее.
Конечно это все для серьезных вещей. Для примитивной поделки, которую 3.5 анонимуса полтора раз глянут — хоть на брейнфаке пиши… только не надо как Саня-фраер при этом гуру кодинга из себя тут корчить.
Зато тут есть проблема — с интерпретатором ты должен заранее на уровне скриптового языка формализовать всё общение. То есть в данном случае, если захочется ему создать карту с хитровыпендрёжным эффектом(а по сути чуть ли не половина карт у него такая), ему придётся добавлять такую функцию в скриптовый язык, всё равно внося изменения в ядро, а затем в карту. Какой в этом смысл?
Ну это и есть говнокодинг — когда берут паттерны и втыкают их, «потому что там так делают», не задумываясь о целесообразности применения.
1) отсутствием огромного мегафайла со всеми картами, в котором сломаешь все, что ломается, вместо которого уютные отдельные скриптики;
2) возможностью менять правила игры не просто не меняя кода программы, а даже в некоторых случаях из нее не выходя, что безумно полезно при отладке.
К тому же никто не предлагает писать на математике прям все. Базовые эффекты реализуешь с помощью функций — так можно будет легко навесить к ним проверки, отладочную инфу, изменить их сразу по всей игре, а не каждую карту отдельно ручками. А сложную уникальную логику пишешь через математику, без чего ты так и так не обойдешься.
А что плохого в одном файле? Нет, можно его разбить на множество мелких, но смысл? Работать человеку удобнее именно с одним файлом. А если сломаешь всё настолько, что оно перестанет собираться — то увидишь это сразу, что тоже плюс вообще-то. Ну и сейчас никто не мешает разбить этот файл на любое количество.
2) Но ведь мы и так не меняем оставшийся код! Если рассматривать этот файл как ресурсы — то мы меняем просто ресурсы. И добавить для отладки смену правил в рантайме так же просто, как вызвать eval(open('CardList.py','r').readAll()) (ну, плюс минус). Так в чём проблема? Зачем lua?
Ну это равносильно добавлению функций на имеющиеся часто повторяющиеся участки Сашиного кода, типа
def battlecry(...):
return battlecry_typical_do_damage(...);
Да, это полезный рефакторинг, я не спорю, но какое отношение он имеет к lua? Зачем для него втыкать второй скриптовый язык?
либо не работал в команде, а вероятнее и то и то
Но есть еще один фактор, не менее важный. Хороший код должен быть внятно структурирован. Отдельные карты представляют собой несвязанные между собой сущности, поэтому их структурно необходимо разделять. Так-то, конечно, можно вообще всю программу в одном файле написать, кот бы спорил, только копаться в этой каше потом нужны будут спелеологи, а не программисты. Если же сущности у тебя грамотно разложены по файлам, а файлы — по папкам, то сама структура каталогов рассказывает о том, как устроена программа.
Но ведь копирование кусков кода — это естественно!
Вот положим, Саша решил сделать из двух карт одну, объединив эффекты. Если он это сделает, объединив куски файла и перенеся код, то на ревизии в версионном контроле это будет отлично видно, что пошло куда, и любой дифф ясно покажет перенос блоков кода. Если он это будет делать пытаясь собрать из двух файлов один с неизвестно какой историей — ну, я не знаю нормальных средств, которые бы это показали, ни меркуриал, ни гит, а уж тем более свн и тфс в такое не умеют. А вы знаете?
Не, ну я не знаю, может у вас опыта в подсасывании и больше, но я не вижу с такими размерами файлов проблем.
Да
Нет
С какого лешего перечисление однородных объектов одного и тот же типа требует структурного разделения? По сути это сущности, объединённые между собой тем, что они элементы одного массива — колоды карт.
Вот, возьмём, к примеру, мой живой код
Вы скажете что каждую строчку надо в свой файл выносить? А то фигли, отдельная сущность, отдельная струтура. Тут же, по сути, происходит то же самое, просто перечисляются сущности со своими константами, у некоторых из них ещё есть коллбеки. Каков смысл этого дробления?
Ты на полном серьезе считаешь, что это — хороший код?
Такой, что один раз добавив принципиально новый эффект, можно будет на его базе легко создать пачку новых карт, с кастомизируемыми вариантами.
И кстати возникновение ситуации «а не добавить ли мне вот такой уберэффект» на этапе кодинга релизной версии — это тоже признак бардака в разаботке. Фундаментальный геймплей отрабатывается на прототипах, а потом уже только правки баланса, типа того же дамага от файрболла.
Но ведь это и сейчас делается
Ха-ха-ха. Вы сторонник водопада, как я полагаю? Интересно, что бы вы сказали, если бы узнали что этой парадигме регулярно не следуют? И даже тот же харстоун, с которого Летающий Александр вдохновлялся, тоже регулярно вводит новые и новые механики, не отнекиваясь «ну у нас тут в ядре не запланировано».
А вот как раз принципиальную фишку они реализовать так и не могут. Чтобы при повторном автоматическом розыгрыше таргетного спелла (например когда отыгрывает йогг-сарон или дрыжеглот) можно было снова таргет выбрать.
Согласен. Но тут дело еще и в личной репутации фраера на табуне. Был бы это чей другой проект — отношение было бы куда позитивней. А Саня — это такая смесь Шарикова из «Собачьего сердца», Выбегалло из «Понедельника» и ослика Иа из «Винни-пуха». Отсюда и к проектам его отношение соответствующее.
Я из аниме смотрел только Хеллсинга и Вампирхантер Ди. Остальное как-то не заходило совсем.
Спойлер
Второй заметно получше.
Не думал чего-нибудь из новенького глянуть? Аниме 90-х и аниме 10-х это две сильно разные вещи.
Хотя всякие там Акиры и Призраки в доспехах я смотрел, но это было давно и не правда)
И вообще, у меня поняши, вместо этих ваших аниме :3
UPD: Да, и, кстати, хоть и с опозданием, но всех присутствующих коллег с праздником ;3
Желаю вам поменьше тыжпрограммирования и побольше… чего-нибудь интересного. Например, нормального программирования :D
всмысле?
Интересно как раз «ненормальное програмирование». Так ингда называют лютые кодовые извраты, которые перподносятся как головоломки или просто занимательные задачки. Квайны например, или выжать максимум из шаблонов С++17… демомейкерство отчасти тоже туда же.
С праздником!