Log of Alprog
1.22K subscribers
90 photos
88 links
Download Telegram
А я начинаю серию технических постов про архитектуру и графику игры. Казалось бы, ну чего там может быть интересного в программировании RPG с пошаговой боёвкой? Тем более на Unity (читай — на всём готовом). А вот оказывается, что кое-что может. На несколько постов точно уж наберётся. Понимаю, что не все мои читатели программисты, поэтому буду маркировать посты, затрагивающие эту тему, хештегами #код или #кодище (в зависимости от интенсивности). Лайтовые посты на общие темы буду помечать тегом #лайт.
Скрипты в Encased
#код
Формально весь код, который мы пишем для Encased — это C# скрипты (в исходники Unity мы не лезем). Но у нас есть два уровня скриптов: механика игры, которую пишут программисты; и «скрипты скриптов», которыми занимаются контент-мейкеры, то есть гейм- и левелдизайнеры. В первую очередь, это сценарии квестов. Чтобы как-то различать, но не изобретать новую терминологию, в рамках компании мы код программистов скриптами не считаем. Это условно просто код игры. А слово «скрипт» применяем только для скриптинга квестов и локаций. Именно о них сейчас и пойдёт речь.

Вообще, я большой и страстный фанат lua. Связку Сpp + lua и вовсе считаю идеальной для геймдева. Господи, у меня об этой парочке даже любовная лирика есть — настолько всё плохо :) Но так или иначе, поскольку мы пишем на Unity, то у нас уже есть связка Cpp + C#. Да, некоторые прикручивают lua к Unity, но обычно это те же самые люди, которые высказываются в духе «C# теснит C++ в геймдеве». То есть в их голове получившаяся схема выглядит, как C# + lua, но на самом-то деле это Сpp + C# + lua. Лично мне, как стороннику принципа KISS, от такой переголовы становится плохо. Каждый понимает этот принцип по-своему (об этом как-нибудь в другой раз), но для меня это означает, что в проекте должно быть как можно меньше лишних сущностей, а, значит, в качестве языка для скриптов мы остановимся на идущем из коробки C#.

С языком определились, осталось эти скрипты спроектировать. И здесь мне очень сильно повезло: в нашей команде есть дизайнеры, которые работали над похожей по механикам и масштабу игрой — над Divinity: Original Sin 2. Поэтому мне не нужно гадать, что им может понадобится, а что — нет; мне достаточно посмотреть все их юзкейсы, скопившиеся за время работы над игрой, и просто реализовать привычный для них функционал. Там y них был свой собственный язык сценариев, я же попробовал адаптировать это к C# и нашей архитектуре, и получилось примерно следующее.

Каждый скрипт (например, квест) — это отдельный класс, унаследованный от класса Script. Каждый скрипт может существовать в проекте только в одном экземпляре. У него есть функция Start() и во включённом состоянии у него выполняется Update(deltaTime). Скрипт может находить игровые сущности в мире (сущности у нас отвязаны от сцен и GameObject’ов и доступны из кода всегда, даже если игрок сейчас в другой локации), назначать этим сущностям задачи в очередь выполнения и подписываться на их эвенты.

Система эвентов представляет собой надстройку на основе делегатов. Можно подписываться, как на события от конкретной сущности, так и глобально на все события определённого типа. Во втором случае можно также добавить подписке фильтры. Например, подписаться на клики по объектам с определённым тегом. Обработчик события при сохранении резолвится в имя скрипта и метода. Да, для совместимости сейвов переименовывать методы нельзя. Во всём остальном проекте их можно переименовывать, потому что они помечаются атрибутами сериализации; но в скриптах решили атрибуцию не делать, чтобы их не усложнять (скриптеры готовы мириться с таким ограничением, а если уж очень надо, то рулить версионность ифами). Также в классе скрипта сериализуются и попадают в сейв все его поля. Для разработчика скрипта это получаются своего рода локальные переменные, где он может хранить всякие временные штуки.

Вот такая примерно система у нас. На мой взгляд, получилось довольно гибко и удобно. Так ли это на самом деле покажет время, когда мы перейдём в фазу активного наполнения игры контентом (пока у нас основная механика и даже архитектура ещё в процессе). Буду держать вас в курсе.
Jai vs C++
#код
На днях все снова заговорили про язык Jai, в связи с новостью о том, что его автор, Джонатан Блоу, собирается выпустить закрытую бету до конца года. Я решил, что дети могут насмотреться его стримов и неправильно понять, и что кто-то из взрослых должен объяснять такие вещи. Не думаю, что в русскоязычном геймдев сообществе наберётся много авторов, которые могут писать про такие темы, поэтому почему бы мне не стать таким человеком?

Начнём с того, что Блоу (это автор Braid и The Witness, если кто не в курсе) один из немногих в индустрии, на которых действительно хочется равняться. Из всех инди-разработчиков его философия и подходы наиболее близки мне. Во-первых, он относится к играм, как к искусству. И здесь я имею в виду, что он стремится, чтобы они таковыми были, но при этом не переоценивает их нынешнюю ценность (включая свои проекты). И, во-вторых, насколько я могу судить, считает написание кода не препятствием на пути чистого творчества, а частью этого самого творчества. Техническое исполнение это не просто средство, это часть самого произведения искусства. Если вы думаете сходным образом, вы поймёте, что красота должна быть везде: и в геймдизайне, и в коде. Они должны дополнять друг друга, работать в ансамбле; одно ради другого страдать не должно. Отсюда и тяга и к написанию идеальных инструментов под себя: чтобы творить в удовольствие, а не идти каждый день на компромисс с самим собой. В этом мы с ним похожи. Только он версия на максималках. Вернее HD ремастер на максималках.

Я пишу движок, чтобы в будущем комфортно было заниматься своими проектами; у Блоу уже есть два мировых хита и он идёт дальше — пишет для себя свой язык, и ничего, кроме уважения, это у меня не вызывает. Но как только кто-то начинает говорить (в первую очередь сам Джонатан), что Jai способен заменить С++ у широкой аудитории (и тем более в ААА), вот тут-то у меня и начинается скепсис. Сколько уже было попыток: D, Go, Rust? По моему мнению, Jai просто пополнит этот список.

Само желание избежать C++, мне понятно. Он причиняет боль, и я бегал от него по всяким бейсикам и шарпам до последнего, так как верил, что их мне хватит. Но чем дольше прятался, тем очевиднее становилось, что мне не хватает производительности и трушной кроссплатформенности. Какой-нибудь маленький инди-проект, конечно, можно написать на D или Rust, но если ты пишешь движок, который собираешься в дальнейшем поддерживать, или если ты большая компания, то это не вариант. Проблемы недостаточно богатой инфраструктуры здесь будут выходить на первый план; ты не сможешь гарантировать, что язык не загнётся, когда ключевых авторов собьёт автобус, и что будет компилятор этого языка под архитектуру процессора новой игровой приставки. Фактически, у тебя только два бескомпромиссных варианта: либо писать свой язык (если ты совсем ниндзя-самурай), либо перестать бояться и полюбить С++.

Пост бы был грустным, если бы не одно «но»: я очень верю в будущее С++. Jai и компания потому никогда и не вытеснят С++, что он не стоит на месте. Сейчас у этих языков куча плюсов по сравнению с плюсами (простите за каламбур), но уже в ближайших стандартах С++ должен обзавестись модулями, концептами и корутинами, благодаря чему уже начнёт ощущаться, чуть ли не как новый язык. Две основные фишки Jai — компайл-тайм выполнение кода и рефлексия — по всей логике будут следующим шагом, как развитие constexpr и некоторых перспективных предложений по статической рефлексии. Даже если на воплощение этого в жизнь уйдёт 10 лет, этого времени всё равно недостаточно, чтобы успеть вытеснить С++ с рынка. Ну а если заглядывать совсем в далёкое будущее (и немного помечтать), то метаклассы Герба Саттера вообще должны перевернуть программирование и убрать всех конкурентов в этой весовой категории.
Сумбурная объяснялка про тайлики и рамочки
#код
Обещался побольше рассказывать про код Encased, а не пишу совсем. Нехорошо. Будем исправляться. Сегодня незначительная, но довольно замороченная тема: как рисуется у нас рамка для зон досягаемости под ногами персонажа.

Начать придётся с конца. Ходят персонажи у нас не по 2D-пространству, а по сложному 3D ландшафту со всякими ямочками, холмиками, мостиками через канаву и вторыми этажами. А потому игровая сетка — штука сложная: не массив, а самый настоящий граф. В большинстве случаев одна игровая клеточка это ровно один квад, но иногда, на всяких неровных поверхностях, это 16 квадиков помельче (4x4). Так или иначе, вертексы этих квадиков имеют две текстурные координаты. Одна хранит локальные координаты внутри клетки, другая же — индекс клетки. Ну просто порядковый номер клетки (интовый), так как граф может иметь самую разнообразную форму и 2D-координаты на него не натянешь.

Ещё есть специальная текстура, которая динамически меняется во время игры и где каждый тексель (по порядковому номеру) указывает шейдеру на то, какой спрайт нужно выводить в этой клетке. У текселя 4 компоненты RGBA, которые прекрасно кодируют Rect спрайта внутри атласа. Более того, как несложно заметить, переставив каналы местами, мы бесплатно получаем ещё и любые зеркальные отражения спрайтов.

Осталось эти спрайты нагенерить. Но не рисовать же все варианты спрайта, правда же? Поэтому делим спрайт на 4 части и рисуем варианты только для левого-верхнего уголочка. У меня получилось 5 видов: пустой, горизонтальная грань, вертикальная грань, обе грани, уголочек (см. рисунок). Это кодируется тремя битами. Кусочков 4, так что полностью спрайт определяется 12-битным числом. Если оставить только возможные комбинации, то их 625 (5^4). Пробегаемся по этим вариантам, проверяем их на валидность (биты горизонтальной и вертикальной линии у соседних кусочков должны совпадать) и генерим спрайты. Попутно, разумеется, чекаем их на зеркальность и составляем мапу (12-битное число -> 12-битное число + вид зеркального отображения). И получается всего навсего 20 уникальных спрайтов.

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

Если вы уследили за моей мыслью, держите пятюню. Если в какой-то момент поплыли — буду рад фидбеку, где именно я пишу непонятно. А если я у вас отбил всякое желание стать графическим программистом, то я не специально.
Пиарчик симпатичного канала
#код
Нравятся вам мои лонгриды с пометкой #код? Если да, то вам стоит также глянуть канал @gamedev_architecture. Это, пожалуй, самый близкий ко мне по контенту канал из тех, что я знаю. Автор тоже фокусируется на всяких интересных решениях в геймдеве с точки зрения программиста, но делает это не в виде фронтовых писем и баек, а оформляет в большие обстоятельные статьи по конкретной проблеме. Технические статьи, признаться, я у него не всегда осиливаю, но философские опусы мне, как правило, заходят. В частности, про командную работу замечательную колонку от 15 мая рекомендую к прочтению людям как с опытом, так и без.
Что-то вроде ECS, но вообще не оно
#код
Этот пост я пишу из Исландии. Коротко сформулировать впечатления от этой страны можно так: красиво, но бессмысленно. Это не в обиду исландцам сказано, просто здесь меня постоянно преследует немой риторический вопрос «зачем вообще селиться на этом суровом куске земли?» Местность вокруг часто больше походит на терраформированный Марс, чем, собственно, на Землю. Но сейчас я «под куполом», то есть в тёплой гостинице, а потому хочется поговорить о чём-то противоположном: пусть не очень красивом, но зато функциональном.

Таковой является, например, наша компонентная система. Последние полгода стоит кому-нибудь заговорить о компонентах, как все сразу вспоминают ECS (Entity Component System), и на первый взгляд то, что написал я, довольно похоже; но на самом деле не имеет с ним ничего общего.

Но давайте по порядку. В Unity много лет были GameObject’ы, которые содержали Component’ы. Это привычная всем, но неэффективная модель. Некоторые даже ошибочно называли её ECS, хотя ей там и не пахло. Настоящая же ECS предполагает 3 вещи: это, собственно, Entity (сущность), Component и System. Причём Entity, в отличие от GameObject’ов — это не объекты. Это просто целочисленные id. То есть просто номера сущностей, которые сами по себе никаких компонентов не хранят. Напротив, каждый компонент «знает», к какой сущности он закреплён. И что немаловажно, компоненты одного типа лежат в одном месте (в одном массиве) и представляют собой только данные. А вся логика находится в системах, которые обрабатывают компоненты только определённого типа. Таким образом логика всегда сводится к тому, чтобы пробежаться по одному или нескольким массивам компонент и выполнить над каждым элементом какие-то однообразные задачи. Благодаря тому, что однотипные данные лежат в памяти друг за другом последовательно, эта задача прекрасно параллелится за счёт векторизации и крайне cache-friendly, что очень и очень хорошо для производительности. В этом и есть основная суть ECS.

В Unity для её внедрения привлекли самого Майка Актона (одного из самых главных идеологов Data oriented design и ECS в частности). Я не смотрел, что у них в итоге получилось, но судя по имени, там теперь действительно всё грамотно в кои-то веки. Но мы стартовали проект, когда этого ещё не было в стабильной версии Unity, поэтому начали разработку на своих компонентах.

У нас тоже есть Entity, но это не номера, а полноценные объекты, которые содержат наши компоненты (мы их зовём модули), а систем нет вообще. По сути это больше даже похоже на старую модель компонентов Unity и тоже совершенно не про производительность. Зачем же тогда надо было это переизобретать? Дело в том, что одним из самых первых и главных стратегических решений в Encased было как можно сильнее отвязаться от сцен и gameobject’ов. Наши Entity и модули — это данные в чистом виде. Игра может играть сама в себя безо всякой визуализации (с небольшими оговорками), а также легко и безболезненно сохранять или загружать своё состояние. С gameobject’ами, как вы понимаете, это сделать было бы в разы сложнее, так как сериализация в Unity это та ещё Песнь пламени и льда (про наше решение как-нибудь напишу отдельный пост). А вся визуализация у нас происходит за счёт размещения на сцене Actor’ов, которые «играют» роль сущностей. У актора на каждый тип модуля сущности есть ModuleSync (если это необходимо), который синхронизирует визуальное представление. Причём могут быть различные Sync для Play- и EditMode.

Таким образом мы минимально завязаны на Unity, минимально имеем оверхедов от её классов, но при этом у нас удобная компонентная система. Потенциально удобная в первую очередь для дизайнеров, так как объекты в RPG играх могут содержать самые разные сочетания свойств, так что без чего-то подобного делать такой проект крайне сложно. К тому же недавно мы добавили возможность наследования компонент от родительской сущности, что теперь позволяет делать шаблоны оружия или предметов прямо на базе компонентной системы.

Обсудить
Мета-мечты
#код
После серии довольно сухих технических статей хочется разбавить блог более живой и личностной записью. Давайте я расскажу вам о том, что последний год вдохновляет меня заниматься программированием. Не геймдевом в частности (там всё понятно), а программированием в принципе. Да-да, возня с циферками и указателями может быть увлекательна сама по себе; но это как космонавтика: интересно осваивать лишь новые рубежи (парадигмы), а курсировать по одним и тем же маршрутам быстро превращается в рутину. Именно поэтому так многие в свой кризис среднего возраста ударяются в функциональщину и тому подобные тяжкие.

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

Чтобы реализовать метаклассы, нужно сперва сделать рефлекшн, расширить выполнение компайл-тайм кода и code-injection, на что уйдёт уйма лет и митингов, но результат стоит того, чтобы к нему стремиться. Метаклассы расширяют представление о возможном также, как первое глубокое погружение в шаблоны (не путать с генериками .Net; они — детский лепет). А, может быть, даже ещё больше.

Кто-то может сказать, дескать, тоже мне, удивил — всё это уже было в Python, Ruby и другой хипстоте. Честно признаюсь, не смотрел, как это выглядит в вышеупомянутых языках, но крышесносящей смесь по моему мнению становится лишь в связке с С++. Высокопроизводительный консервативный и, если хотите, задротский язык, который терпеть не может платить за то, что не использует, казалось бы, обречён быть неповоротливым и жестоким. Но он полностью преображается с метаклассами и при этом не изменяет своим фундаментальным принципам. Это полностью меняет правила игры и должно стать для многих вторым открытием языка (именно поэтому я верю, что с++ не исчезнет из топов даже в долгосрочной перспективе).

Итак, что же это за хрень, которую я так долго нахваливаю? Суть в общем-то сводится к тому, чтобы позволить, не выходя за пределы языка, обрабатывать классы во время компиляции. Например, пробежаться по всем членам класса и изменить их модификаторы доступа, добавить или удалить конструкторы, автоматически сгенерировать перегрузки функций сравнения или бросить ворнинги, если чего-то не хватает. Если поразмышлять немного над тем, что это даёт, то можно понять, что это открывает поистине потрясающие возможности: мы сможем сами ввести в язык понятие, скажем, valuetype (со всеми вытекающими требованиями) или interface. При этом не будет бесконечных споров в комитете, что именно правильно подразумевать под этими словами и достаточно ли это универсально. Комьюнити само со временем наработает всевозможные паттерны, лучшие из которых впоследствии войдут в различные библиотеки, и никто не уйдёт обиженным. То есть мало того, что возможность завести valuetype и иже с ними сама по себе привлекательна, так ещё и сам язык начнёт эволюционировать динамичнее.

Впрочем, мне через 5 минут уже пора на посадку в самолёт, а вам более подробно (и чертовски наглядно) всё объяснит сам Херб Саттер в этом видео. Ну а самые любознательные могут также заглянуть в соответствующий пропозал.

Обсудить
Что учить, чтобы вкатиться в геймдев?
#код
Как я упоминал ранее, вопрос «с чего начинать игровому программисту» один из самых частых, что я слышу. И сегодня я, как обещал, напишу развёрнутый ответ (чтобы потом на него всегда ссылаться) и даже расскажу оптимальную на мой взгляд последовательность действий.

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

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

Но если человек хочет всего того же самого, но не стать при этом юнити-программистом в плохом смысле слова и готов ради этого страдать продолжительное время, то ему стоит выбрать другую дорожку. И в этом месте может показаться, что это более правильный подход (мы же на харде привыкли играть, ага), но не нужно обманываться. Разница между группами принципиальная: если вы считаете, что главное игра, а как она там внутри написана неважно, то вам не по пути. Для вас есть Юнити. И не нужно себя ломать. Зачем зазря усложнять себе жизнь? Для тех же, кто остался, кому принципиально важно писать качественный код и быть хорошим программистом (с точки зрения других программистов, а не менеджера Васи), я предлагаю следующий маршрут:
Скрипты в Encased 1.1
#код
Что-то у меня одновременно много людей заказали размещения, поэтому чтобы канал не превращался в сплошную рекламу, придётся делать посты почаще на этой неделе.

А давайте поговорим, например, про скрипты. С прошлого раза, когда я про них рассказывал, они претерпели некоторые изменения и это должно быть интересно. Напомню, что у нас скрипты — это экземпляры классов, унаследованных от Script, которые имеют функции Start() и Update(), могут содержать сериализуемые поля, которые выполняют роль эдаких «локальных переменных», а также скрипты умеют подписываться на различные события от сущностей в мире.

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

Их основная идея была в том, что мы подписываемся на нужные события в Start и потом, по мере обработки событий, отписываемся от них или подписываемся на новые. При этом функция Start, разумеется, больше никогда не вызовется. Даже после нажатия Сохранить/Загрузить мы получим абсолютно то же состояние подписок, которое было до этого. Но скриптеры ожидают, что они могут сохраниться, добавить подписку в функции Start, загрузиться и обработать свежедобавленый эвент. Этой ситуации у нас пока не случилось, но я предвосхищаю, что мне бы пришёл вопрос «а нельзя ли сделать, чтобы после загрузки Start() отрабатывал ещё раз?». То, что тогда получится по 2 подписки и это вообще ломает всю концепцию скриптов, никого бы не смутило.

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

Например, мы помечаем функцию атрибутом
[OnUsed(Guids.Levels.MagicBalls.LeverSwitch)]

что означает, что функция отработает, когда мы нажмём на рубильник в игре. LeverSwitch — это константа, содержащая уникальный guid рубильника, который доступен из скриптов, потому что у нас сделана кодогенерация констант (с поддержкой последующего переименования, разумеется).

Реальной отписки от этой функции причём не существует, но она эмулируется через проверку глобальной переменной внутри тела функции или с помощью атрибутов. Вроде таких:
[If(BunkerVariables.LiftEnabled)]

или ещё проще
[OnlyOnce(0)]

Последний вариант означает, что функция выполнится только один раз, а 0 нужен для уникальной пометки этой функции (чтобы можно было безопасно переименовать функцию и ничего не сломать). В пределах одного файла у других функций, соответственно будет OnlyOnce(1), OnlyOnce(2) и так далее.

На поверку оказывается, что если функция не вызывается по причине того, что на её запуске стоит тест переменной, которая в данный момент не включена, то это намного проще воспринимается скриптерами, чем функция, которая не вызывается, потому что на неё никто не подписывался в истории этого сейва. Так что такая модель выглядит более перспективной в плане поддержки игры и накатывания апдейтов. Посмотрим, как она покажет себя в бою, а пока всем счастливого рождества!

Обсудить
Подробности про командную строку
#код
Запилил на днях по работе командную строку с проверкой синтаксиса и автодополнением и похвастался этим в чатике. Народ проявил интерес и поэтому рассказываю подробнее.

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

Теперь о реализации. Несмотря на кажущуюся сложность задачи, имплементация у меня до боли простая и занимает чуть более тысячи строк. Первый элемент системы — это, конечно, лексический анализатор. Который у меня в коде какого-то фига называется Parser (надо не забыть переименовать в Lexer, а то чё я, как наркоман). Лексер представляет собой одну единственную функцию-генератор, которая в цикле читает символы строки и возвращает по одной токены лексем, которыми могут быть:
Value (строки, числа, true и false),
Identifier,
Dot,
Comma,
OpenBracket,
ClosedBracket,
Operator,
AssignmentOperator,
EndOfLine

Между лексемами могут быть пробелы, они игнорируются. Помимо типа, токен также хранит начало и длину фрагмента внутри входной строки (чтобы подсветить красным место, в котором произошла ошибка) и поле типа object для дополнительной информации: для value это считанное значение, для оператора — информация о том, какой конкретно оператор и т.п. Единственный нюанс, при чтении знака минус, нужно взглянуть на то, какой токен был перед этим: если значение, идентификатор или закрывающая скобка, то это оператор минус, в противном случае начало отрицательного числа.

Едем дальше. Непосредственно сам интерпретатор совмещённый с валидатором. Он у меня однопроходный, то есть я читаю очередь лексем слева направо и сразу же выполняю. Командная строка может выполнять только выражения. Выражение — это один или несколько операндов, разделённых операторами. Например,
a + b * c + d

или просто
a


Когда мы дошли до конца выражения (конец строки, запятая или закрывающая скобка), мы выполняем операторы в порядке их приоритета («схлопываем» по два операнда, пока не останется только один).

Операндом может выступать как значение, так и другое выражение в скобках. Поэтому если наткнёмся на открывающуюся скобку, то просто запускаем процесс парсинга вложенного выражения рекурсивно. И также операндом может выступать цепочка идентификаторов, типа такой:
Foo.bar.foo(a + b, c).foo.bar


В данном случае мы тоже выполняем всё последовательно. Сначала ищем объект среди глобальных. У меня разрешены только классы скриптов (считай, синглтоны) и enum’ы. Затем на каждый доступ через точку достаём через рефлекшн соответствующий member класса, а при вызове функции запускаем сперва вложенный парсинг выражений-аргументов через запятую.

Собственно, всё. Ну ещё есть оператор присваивания, который умеет вызывать сеттер для поля или свойства, но теперь точно всё.

Валидация происходит точно также, как и выполнение, только в «холостом» режиме: вместо честных вызовов функций и операторов, мы возвращаем объект-заглушку, которая знает какого типа должен быть результат. Этой информации достаточно, чтобы проверить весь синтаксис.

Автодополнение же сделано запуском валидации строки, в которую в определённом месте вставлен символ многоточие. При разборе
GlobalVars.Pl…

лексер вернёт не идентификатор «Pl», а специальную лексему типа Autocomplete «Pl...». Ну а синтаксический анализатор, если наткнётся на эту лексему там, где предполагается идентификатор, посмотрит, какие вообще есть варианты, и если что-то подходит, то бросит AutoCompleteException, содержащий остаток строки. Заменой выделенного текста на автодополненный вариант занимается уже гуишный контрол наверху.
SOLIDные рассуждения
#код
Месяц назад я твитнул, что, дескать, как только слышу от соискателя или работодателя в геймдеве что-нибудь про SOLID, так сразу автоматически помечаю их в своей голове как нубов, либо поехавших. С тех пор мне пришлось пару раз подискутировать на эту тему в разных уголках интернета, и я решил, что стоит уже это оформить в отдельный пост.

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

Поехавшие, в свою очередь, делятся, на две основные категории. Первые — это люди, пришедшие из энтерпрайза. Я никогда в энтерпрайзе не работал, но похоже, что там написание абстрактного кода эволюционно более привлекательная стратегия. Я ничего против этого не имею, но когда эти люди приходят в геймдев, с ними бывает тяжело сработаться, так как это зачастую сплошное ООП головного мозга. Там, где типичный программист из кровавого энтерпрайза в рубашке с длинными рукавами заведёт десять интерфейсов и фабрику фабрик; типичный геймдевелопер в перепачканной пиццой футболке вооружится принципами KISS и YAGNI и удалит это всё нахрен, написав взамен простой и понятный класс, решающий конкретную задачу. Пусть и наплевав при этом на принцип открытости/закрытости, разделения интерфейсов и другие святая святых ООП. То есть, конечно, наверняка где-то в природе существуют игры, написанные полностью по канонам SOLID. Но, во-первых, я такого не встречал, а во-вторых, боюсь представить, что там за монструозная кодобаза, и не считаю это нормальным.

Вторая важная категория «поехавших», которую хотелось бы разобрать — это люди, утверждающие, что применяют принципы SOLID, но на самом деле этого не делающие. Их аргументация обычно сводится к тому, что солид следует применять не по всему проекту, а лишь в тех местах, где он подходит. Но тут налицо непонимание разницы между паттерном и принципом. Солид это не паттерн, а принцип. Ты либо стараешься следовать ему везде, либо это уже не солид.

Поясню. Вот есть DRY. Очень простой принцип о том, что надо стараться избегать копипаста. Конечно, где-то в коде могут быть отступления от этого правила, но если человек исповедует dry, то в какой-то момент он может посмотреть на некий участок кода, решить, что он недостаточно dry, и переписать его менее влажно. Не потому, что этот участок кода работал плохо, а только лишь потому, что он не соответствует принципам, по которым мы решили писать код. И это действительно происходит в реальных проектах. То же касается KISS и YAGNI. Но не SOLID. Не бывает такого, что чувак в геймдеве смотрит на код, и решает, что дай-ка я его перепишу, потому что он какой-то недостаточно солидный. Ну не бывает такого на практике. А если ты так не делаешь, значит, ты и не следуешь этому принципу. У тебя просто в случайных местах код написан в соответствии с его правилами, но ты ими на самом деле не руководствовался.

Вот так и получается, что если кто-то в геймдеве на полном серьёзе топит за SOLID, то для меня это или нуб или поехавший.

Обсудить
Исходники командной строки
#код
Удивительное дело, но моего недавнего поста про командную строку в нашем проекте оказалось недостаточно, чтобы утолить к ней интерес. Народ продолжил требовать запилить ассет. С ассет стором я, конечно же, возиться не стал, но код, так уж и быть, заопенсорсил.

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

Обсудить
GoTo the Dark Side
#код
Сегодня чатик что-то беснуется по поводу использования goto. Орден джедаев не позволяет использовать этот оператор в личных целях, а я же призываю вас перейти на Тёмную сторону Силы.

Но обо всём по порядку. Мой преподаватель программирования на первом курсе университета говорила, что goto в рамках учебного курса применять нельзя, и что она не будет принимать лабораторные работы с ним. И это очень правильно: личинкам кодера нельзя давать в руки такой инструмент, иначе они обмажут им все стены. Чтобы стать хорошим программистом всё-таки нужно сперва научиться писать код без goto.

Проблема начинается в тот момент, когда забывают добавить волшебную фразу «в рамках учебного курса». Людям преподносят goto как абсолютное зло, которое нельзя допускать ни в коем случае. Это превращается в примитивную пропаганду и, судя по количеству приверженцев идеи, так происходит достаточно часто. Да простят меня читатели за то, что я сейчас сворую блок мыслей у Андрея Коняева, но всё дело в том, что пропаганда, какие хорошие практики она бы не пыталась прививать, всё равно остаётся пропагандой. И ничего путного из этого не получится. Пропаганда даёт человеку позицию, но не объясняет её; и на выходе мы получаем кучу людей, которые не способны ответить на вопрос, почему они делают то, что делают. Максимум, что мы услышим, это лозунги вида «goto — это зло», «goto ухудшает читабельность!». Любая попытка разобраться, а действительно ли пострадала, например, читабельность исходников lua из-за того, что там обработка ошибок происходит через goto, будет встречена лишь непониманием и хейтом: «А разве не очевидно? Тут же стоит goto. А это худшая практика из всех. Или ты чё, защитник goto?»

Вам может сейчас показаться, что я утрирую, а на самом деле ненависть к goto обоснована и никакого фанатизма нет, но давайте проанализируем. Вы без труда можете представить человека (а может сами им являетесь), который ненавидит всей душой goto, но при этом любит и обильно использует макросы. Но если вдуматься, негативные эффекты от злоупотребления этими вещами крайне схожи. В обоих случаях от одного использования не случится никакой катастрофы, и даже более того, этим можно сильно облегчить себе жизнь и повысить читабельность в конкретном месте. В обоих случаях, если не локализовать применение, можно нарваться на опасные сайд-эффекты. И в обоих случаях, если пихать повсеместно, проект очень быстро превращается в неуправляемую кашу. Но в первом случае человек борется до последнего против появления в проекте «вредного сорняка», а в другом смотрит сквозь пальцы: не, ну а чё такого? Макросы же не включены в расстрельный список — значит, можно обмазываться.

Переходите на Тёмную сторону Силы и начинайте использовать не хорошие или плохоие практики, а здравый смысл.

Обсудить
Про бег и уравнения
#код
По моим наблюдениям практически единственное, что у людей остаётся в памяти из школьной программы по математике — это как находить дискриминант квадратного уравнения. Забавно, но при разработке игр это как раз надо крайне редко. Я вот, например, без гугла и не скажу, где там минус и из кого. А вот тригонометрию всякую помню наизусть, потому что нужно каждый день. Но иногда всё-таки дискриминант тоже нужен. Сейчас приведу пример. Собственно, весь дальнейший пост будет просто иллюстрацией типичного случая, когда нужно немножко математики (серьёзно, ничего другого не будет).

Вот потребовалось мне сделать, чтобы человек на бегу останавливался синхронно с анимацией. У нас в аниматоре есть параметр скорости движения. Один метр в секунду — лёгкий шаг, два быстрый, три и более — бег. И так настроено, что ноги двигаются аккурат с этой скоростью. Резко этот параметр дёргать нельзя (будет скачок), надо плавненько. Но и когда моделька на месте стоит, долго крутить ползунки тоже нельзя — будет ногами в воздухе болтать. В идеале надо чтобы всегда скорость движения персонажа совпадала с анимацией. То есть для этого надо начать тормозить чуть заранее фактической остановки.

А двигается персонаж у нас по точкам. От точки к точке по прямым линиям. Если точка последняя в маршруте или там очень резкий поворот, то, очевидно, в этом месте персонаж должен полностью остановиться (и развернуться). Если же поворот не сильный, то можно лишь слегка притормозить, а то и вообще пройти поворот на максимальной скорости.

Вот и получается, что в каждый момент времени у нас есть текущая скорость, расстояние до ближайшей поворотной точки и скорость, которая должна быть на финише. Если персонаж разгоняется до максимальной скорости, скажем, за один метр пути, а тормозит за два, и при этом у нас 100 метров до финиша, то вопросов не возникает. Но как быть, если расстояние до следующей маршрутной точки всего метр? Сколько времени мы можем позволить себе разгоняться на этом пятачке, прежде, чем начать тормозить, чтобы остановиться точно в конце? А может у нас уже ненулевая скорость и вообще нет времени сопли жевать и пора экстренно тормозить? Может быть даже резче обычного (если дверь закрылась прямо перед носом). Ну и, конечно же, надо, чтобы всё эта байда от FPS никак не зависела.

Как быть в этой ситуации? Известно как: составлять систему уравнений и решать, как в школе. Сперва я, правда, вычитаю дистанцию, которую персонажу в любом случае надо пройти, чтобы компенсировать разницу между начальной и конечной скоростями. Так что в начале и в конце манёвра мы имеем одинаковую известную скорость v1. А в конце ускорения (и перед началом торможения) неизвестную скорость v2. Ещё нам известна общая длина пути s, а также a1 и a2, то есть ускорения разгона и торможения соответственно, но неизвестны их длительности t1 и t2.

Записываем это в виде формул равноускоренного движения и понеслась. Я имею привычку набрасывать решения в paint.net, так что весь процесс вы можете наблюдать в шапке поста. В конце остаётся посчитать коэффициенты и найти корни квадратного уравнения.

Вот примерно так у нас теперь персонажи ходят. Всё, правда, несколько сложнее, потому что надо ещё погемороиться с поворотами, но это уже другая история.

Обсудить
Две истории моей юности про вирусы
#код
Чатик ожил и пристыдил меня за молчание на канале. Что ж, давайте я вам расскажу две истории про вирусы. В первой я буду выступать злостным создателем вредоносного ПО, а в другой, напротив, окажусь доблестным антихакером (блин, количество баек, которые я могу рассказывать в барчиках стремительно сокращается из-за бложека).

Когда мне было лет 14, я уже два года писал примитивные игры на бейсике, делал карты для Half-Life (иногда даже на заказ), и тому подобные вещи. Словом, файлы моего производства распространялись среди знакомых довольно широко и запускались несмотря на угрозы антивируса без подозрений. А ещё мы тогда играли в StarCraft через HyperTerminal (некоторые подписчики, наверняка, таких слов даже не слышали, но это была такая штука, позволяющая соединяться в локальную сеть через телефонные модемы). Само собой, я тоже захотел сделать сетевую игру. После успешного релиза и плейтеста моих крестиков-ноликов на WinSockets, у меня появился мой первый (и последний) коварный план по написанию вирусов. Идея была поистине хитроумной для моих лет. Я собирался написать клиент шахмат, который бы также позволял лазить по файловой системе другого игрока, пока ты якобы размышляешь над ходами. Му-ха-ха! Оценили масштаб вероломства?

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

Первая история была для разминки, но вторая будет более захватывающей. Родители никогда не воспринимали всерьёз моё увлечение разработкой игр, поскольку не особенно отличали это занятие от собственно игр, долго отговаривали поступать на программиста, но в какой-то момент всё же смирились и даже в середине первого курса подарили мне личный ноутбук. Даже у старшего брата не было отдельного компа, а у меня был. И я с гордостью таскался с этой почти 5-килограммовой махиной в универ. Опять же, некоторые подписчики могут офигеть, но тогда это был нормальный вес для ноутбука и в них, представьте себе, пихали даже CD-ROM.

Так вот. Родители на тот момент хотя и расщедрились, но всё же не особо видели во мне программиста. Всё изменилось, когда однажды родители подхватили вирус. Стандартное по тем временам дерьмо про то, что комп заблокирован, и вы должны куда-то там перевести деньги. Курсор не реагирует, диспетчер задач не вызывается, безопасный режим не помогает. Переустанавливать винду не хочется.

В определённый момент я запихиваю в привод диск какой-то игрушки и слышу, что играет музыка автоплея, хотя самого окна на экране нет. В это мгновение я понимаю, как вирус работает. Я тогда как раз читал книгу форума VBStreets — сборник различных нетривиальных ухищрений с Visual Basic. Среди всего прочего там был пример по созданию дополнительных рабочих столов в Windows. Если кто не знал, такая возможность программно была встроена даже в XP, просто не была протянута в интерфейс. И вот то, как вёл себя вирус, было чертовски похоже на запуск примеров кода по рабочим столам (если тебя переключили на другой стол, ты никак не можешь повлиять на другие).

Что же делаю я? Я сообщаю родителям, что могу написать антивирус, сажусь за ноутбук, пишу за час-другой программу, которая создаёт новый десктоп и открывает там окно-переключалку между столами; создаю файл autoplay и записываю его на болванку. Затем вставляю свежезапечённый диск в заражённую машину и — о чудо — на экране появляется окошко с кнопкой, возвращающей управление компьютером. После чего уже руками нахожу и убиваю остатки вируса. Стоит ли говорить, как после этого выросло уважение ко мне со стороны родителей?

Обсудить
Переобувания Блаба
#код
Что-то нашло настроение поразмышлять о том, как часто я менял мнение на программерском поприще. Такое на самом деле случается нечасто, потому что мы все живём в пузырях из собственного опыта. Первый инструмент или подход, который хорошо себя зарекомендовал, формирует в нас эффект утёнка. Затем мы этим самым утёнком какое-то время восторженно забиваем все гвозди в округе. И вот спустя десятки птичьих, натянутых на разнообразные модели Земли, когда по-хорошему стоило бы задуматься об этичности происходящего, у нас уже достаточно травматичного опыта, чтобы спутать стокгольмский синдром с любовью.

Когда твоя позиция сформулирована и озвучена коллегам возле кулера, когда по теме написаны посты и твиты, бывает уже очень трудно выйти на следующий виток спирали понимания и начать декларировать противоположное своим же вчерашним взглядам. Самая главная моя ошибка прошлого — это конечно то, как упорно я в юности не признавал, что Visual Basic плохой язык (если вам резануло ухо слово «плохой», то переформулирую: неподходящий для моих задач). Осознание этого было длительным и болезненным. Я топал ножкой и капризничал, лишь бы не учить С++. Но сегодня я фанат крестов. Я добрался сюда через пару витков .Net, но что-то мне подсказывает, что на этом моя личная спираль обрывается. Конечно, сейчас я привязан к С++ во многом стокгольмским синдромом, но как мне кажется, у меня есть и рациональные аргументы, почему оглядываясь назад через 10 лет я не сочту это ошибкой.

Другие примеры изменения моего мнения не столь драматичны, но тоже случались. Например, я помню, как 10 лет назад вкручивал локализацию в игру для Wii. И я тогда на полном серьёзе втирал, что для геймдева строки лучше хранить в 8-битных кодировках с переключением кодовых страниц (Windows-1251, 1252 — вот это вот всё). Мне тогда представлялось важным иметь доступ к нужному символу по смещению в строке, а необходимость помнить кодировку совсем не смущала и не казалась геморройной. Я проникся силой юникода уже буквально через год, но стыдно до сих пор.

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

Это далеко не самый интересный для чтения мой пост получился: всем, в общем-то, плевать, какие конкретно метаморфозы проходило чужое мнение. Но как мне кажется, это довольно занятная тема, чтобы порассуждать и проанализировать свои взгляды. А вы помните, как кардинально меняли своё мнение по холиварным темам?
Логарифм и резинки
#код
Как-то в одном из чатов обронили фразу: «а когда вам последний раз был нужен логарифм?». Забавно, но мне он потребовался буквально на следующий день. Это ещё один маленький пост о том, какого рода математика нужна геймплейному программисту в повседневной жизни.

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

Новички обычно просто домножают скорость дракончика на расстояние до игрока. Это простое решение, но ужасно плохое, поскольку время, за которое дракончик долетит до игрока, будет напрямую зависеть от FPS. При низком фпс он будет добираться до цели быстро, а при большом топтаться на месте неожиданно долго. А если FPS скачет, то и вовсе придётся лицезреть нечто рывкообразное.

Юнитологи классом повыше обычно используют небезызвестную функцию SmoothDamp. Внутри там скрывается мудрённое решение из книги Game Programming Gems 4. Вот только нам приходится где-то хранить текущую скорость для каждого процесса сглаживания, да и в целом довольно страшно выглядит. Нельзя ли как-то попроще сделать и без лишних переменных в местах вызова?

На самом деле если мы задумаемся, как будет выглядеть FPS-независимый способ приближения со сглаживанием, то быстро поймём, что нам надо проходить одинаковую долю расстояния за одинаковое время. Например, за первую секунду проходим половину пути, за вторую секунду половину от половины, то есть остаётся четверть, затем 1/8, 1/16 и так далее. И никогда мы по настоящему не достигаем цели, но нам это и не надо. При таком движении неважно в какой точке этого процесса мы оказались (на первой секунде, второй и т.п.), мы всегда знаем, как рассчитать движение дальше. От пути всегда остаётся лишь

1 / 2^t

А значит пройденное расстояние от времени вычисляется по формуле:

1 - 1 / 2^t

Двойка здесь всего лишь указатель на то, что в качестве одинаковых промежутков мы выбрали половину расстояния. Мы можем подставить туда 3, чтобы получить треть, или любое другое число больше 1. Можно думать об этом числе, как о степени агрессивности нашего In в нашем FPS-независимом сглаживании (аналог InCubic, InQuad и т.д.). Формула продолжит работать.

Но для полного счастья нам не хватает настройки времени, за которое дракончик будет визуально догонять персонажа из любой точки. Конечно, полностью он догнать не может, но нам хватит преодоления, скажем, 98% пути:

1 - 1 / base^t = 0,98
base^t = 1 / (1 - 0,98)
t = log(1 / (1 - 0,98), base)

Ну вот и всё. Теперь мы можем инициализировать этими параметрами нашу бесконечную резинку-пружинку, после чего ей можно будет скармливать deltaTime, а в ответ получать LerpK. Таким образом получилось простое FPS-независимое сглаживание для всего, что можно лерпать. Финальный класс можно видеть на скриншоте.

По-моему, симпатично получилось. А вы что думаете?
Поправочка!
#код
Ох, что-то я написал этот пост и тут же понял, что лишнего наворотил. Движение, при котором мы за равные промежутки проходим половину, затем половину от остатка и так далее — это фактически тоже самое движение, когда мы за равные промежутки проходим треть, затем треть от остатка и так далее. Это просто свойство перевёрнутой экспоненты самой по себе. И неважно какое основание. А чтобы управлять агрессивностью In-прыжка, достаточно слегка менять threshold-порог. Чем выше выставить процент порога, тем агрессивнее будет прыжок в начале.

Правду говорят, что если хочешь в чём-то разобраться сам, то расскажи это другому. Вот и здесь так получилось.

public struct EndlessSpring
{
private float TimeScale;

// time is amount of time that needeed to pass
// threshold-share of the distance (never pass 100%)
public EndlessSpring(float time, float threashold = 0.98f)
{
var expectedValue = 1 / (1 - threashold);
this.TimeScale = Mathf.Log(expectedValue, 2) / time;
}

public float GetLerpK(float deltaTime)
{
return 1 - 1 / Mathf.Pow(2, deltaTime * TimeScale);
}
}
Вы точно понимаете корутины?
#код
Много-много лет назад, когда я первый раз пришёл на настоящую работу программировать за настоящие деньги, меня научили корутинам. Ну, не то, чтобы прям научили. Просто там был проект с корутинами, и мне волей-неволей пришлось вникнуть, что это за yield такой непонятный. Новый мир, зазиявший передо мной, в корне перевернул моё тогдашнее представление о том, как можно писать геймплейный код. Я и до сих пор это воспринимаю, как одну из важнейших ментальных ступенек для программиста.

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

Но эти времена давно прошли. Теперь каждый знает, что такое StartCoroutine() в Unity. Все к ним привыкли, и никого ими не удивишь; но я заметил, что самое крутое назначение корутин все ещё ускользает от многих в наше время.

Новички считают, что это просто удобный способ делать задержки. Каждый второй, делающий тестовое про самолётик в DarkCrystalGames, делал таймаут выстрелам через запуск корутины и WaitForSeconds(). Мой внутренний эстет при чтении такого кода всегда бьёт себя по лицу, но это вкусовщина. А проблема в том, что эти люди, как правило, только таким использованием и ограничиваются. Ничего более интересного, чем задержки, на корутинах и не пишут. Какое неуважение.

Но этого поста не было бы, если бы проблемы с пониманием возникали только у новичков. Опытные сеньористые ветераны порой думают о корутинах, как о неком легковесном способе организации мультипоточки.

Нет, нет и ещё раз нет! Мультипоточка тут вообще ни при чём!

Недопонимание происходит из-за того, что к одной и той же вещи можно прийти с двух сторон. Можно с одной стороны постепенно облегчать мультизадачность и повышать безопасность и прийти от потоков к так называемым Green Threads. А можно вообще идти с другой стороны, пытаясь в однопоточном приложении улучшить читабельность колбеков и стейт-машин, и прийти внезапно к тому же самому. В этом случае результат скорее назовут корутинами. Разница между получившимися вещами с точки зрения функционала будет весьма условна, но отличается мотивация и назначение. Если вы познакомились с корутинами не с той стороны, то эта статья для вас.