Log of Alprog
1.19K subscribers
98 photos
91 links
加入频道
А я начинаю серию технических постов про архитектуру и графику игры. Казалось бы, ну чего там может быть интересного в программировании 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, содержащий остаток строки. Заменой выделенного текста на автодополненный вариант занимается уже гуишный контрол наверху.