Большинство игровых движков прошли довольно длинный путь по преодолеванию "детских болячек"; новый же движок (путь, по которому пошёл я) всеми этими преимуществами, увы, обладать не может. Главная сложность сводится к тому, что любая классная идея может иметь непредсказуемые последствия. Это, обычно, приводит к переписыванию значительной части кода.
Лет 20 назад была популярна многослойная архитектура приложений, когда каждый слой отвечал за что-то своё и имел слабую связанность со смежными слоями. Проще говоря, в любой момент можно было изменить (а порой и просто убрать) один из слоёв без необходимости переделывать всё приложение. Потом начали выплывать недостатки такого подхода (в достаточно сложном приложении "связи" в обслуживании становились чрезвычайно трудоёмкими), да и мода сместились на архитектурные подходы с другими названиями и немного изменённой логикой работы.
Но сама идея многослойности имеет много полезных возможностей, если подходить к ней чуть более гибко и не ждать чуда.
В самом начале разработки у меня не было никакой многослойности. Предполагалось, что есть движок (эдакий монолит) и есть пользовательский код, обрабатывающий всю логику игры. И сразу же стало понятно, что движок получается либо крайне скудным (для максимально широкого использования в разных типах игр), либо крайне регламентированный и ограниченный (заточенный под определённый тип игры). А хотелось чего-то более взвешенного.
Итак, первая моя ошибка была в том, что я попытался сесть сразу на несколько стульев. Решением данного вопроса могла бы стать последовательная, поэтапная сборка игры.
Я сознательно решил вернуться к концепции слоёв, но не в том смысле, в каком они использовались на заре веб разработки. Каждый слой - это отдельный компонент, умеющий что-то особенное (скажем, воспроизводить музыку), но не имеющий зависимостей от других слоёв. Однако, каждый слой зависит от предыдущих и работает с ними параллельно. Предположим, звуковой слой работает синхронно со слоем отрисовки и выдаёт необходимое звучание строго в тот момент, когда это необходимо.
Ещё раз: все слои работают одновременно; они синхронизируются друг с другом, могут использовать данные соседних слоёв, но о существовании друг друга даже не подозревают. Синхронизация же обеспечивается неким суперслоем; слоем, работающим как и любой другой, но стоящим отдельно.
В идеале, каждый слой может разрабатываться независимо, а версии для разных игр и платформ должны заменяться по необходимости (например, слой вычислительной математики для 2d и 3d игр будет существенно отличаться).
Наверное, концепция слоёв чем-то напоминается музыкальный редактор, когда несколько инструментов воспроизводятся одновременно.
Итак, слои
Слой первый. Хранилище ресурсов
Практически в любой игре есть некоторый модуль, отвечающий за ресурсы. В принципе, он позволяет делать следующее: загружать и оперировать графикой, звуками, музыкой и прочими бинарными или текстовыми данными. Через него можно работать с текстурой для трёхмерного объекта, а можно - со скриптами ИИ персонажей. Единственное, что от него требуется - некоторый набор метаданных, который может отличаться от приложения к приложению.
Приведу пример: есть персонаж, для которого существует анимация (например, движения). Анимацию можно генерировать пользовательским кодом, а можно задать в хранилище ресурсов на основании спрайтов персонажа.
По сути, хранилище ресурсов - это тот компонент игры, который и делает игру игрой. Изменение метаданных могут влиять на ход игры невероятно гибко без необходимости менять кодовую базу.
Однако, тут есть и обратная сторона медали. Если перестараться и переместить слишком много в хранилище ресурсов - оно станет слишком неповоротливым и тяжёлым и начнёт влиять на работу всего приложения. В идеале, это должен быть исключительно поставщик данных, без права влияния на ход выполнения приложения.
Слой второй. Графика
Хотя это самый важный для игрока слой, он, тем не менее, довольно простой. Главное - управляет положением камеры, смещением экрана и рисует все заранее предоставленные изображения и/или шейдерные программы (сами программы, вероятней всего, будут задаваться извне). Короче говоря, графический слой будет сводиться всего лишь к обёртке интерфейса отрисовки (webgl например).
Слой третий. Звук
Данный слой получает на вход набор звуковых ресурсов и воспроизводит их по запросу. Также, имеются некоторые правила смешения звуков (например, звук взрыва приглушает фоновую музыку и тому подобное); что-то генерируется налету, по мере необходимости.
Впрочем, данный слой для меня представляет, пока что, тёмную материю, т.к. именно со звуком я работал меньше всего.
Слой четвёртый. Взаимодействие и координаты
Фактически, данный слой занимается математическими расчётами и взаимодействию моделей игровых объектов. Собственно, именно тут и происходит вся игра, тут хранятся габаритные коробки объектов, тут определяется, должен ли герой провалиться вниз, или под ним пол, тут фиксируются столкновения персонажей и запускаются обработчики этих столкновений. Наверное, это самый сложный слой для реализации и, хотя его надо в первую очередь выделить и сделать самым абстрактным, именно он определяет тип игры и главные правила и именно он самый сложны для реализации (по сути, именно он и является движком всей игры).
Слой пятый. Ввод-вывод
Назван красиво, но это, фактически, слой управления игрой: обработка нажатия клавиш, мышки или иных средств ввода. Выделение этого слоя позволяет легко портировать игры на разные платформы: компьютер, ноутбук, смартфон. Переоценивать ценность этого слоя не стоит: он, пожалуй, будет самым простым в реализации, однако на нём можно (и стоит) реализовывать различные шаблоны управления игры, которые сделают игру либо крайне удобной, либо вообще неиграбельной.
Слой шестой. Управление слоями
Этот слой отвечает за синхронизацию всех остальных слоёв. Именно он, по сути, содержит основной игровой цикл и вызывает, по необходимости, те или иные действия, равно как и определяет порядок их взаимодействия. С точки зрения игры - это тот слой, который собирает игру воедино. Именно он содержит ключевые правила взаимодействия слоёв, управляет ими, но, при этом, совершенно не содержит никакой игровой логики.
Слой седьмой. Пользовательский
А вот тут находится игровая логика непосредственно. По сути, это псевдослой, который через хуки внедряется в шестой слой, корректируя управления и определяя, какие данные куда и в каком количестве следует отправить. Если шестой слой можно сравнить с машиной, то седьмой слой - с водителем, определяющим, куда надо ехать. Данный слой хранит состояние игрового процесса, но не имеет прямого доступа к другим слоям.
А теперь про проблемы
Концепция семи слоёв очень притягательна. Ведь здорово, когда у тебя есть слой, который в нужное время просто исполняет то, что от него требуется, а потом ждёт. Но, тут, увы, не обошлось и без ложки дёгтя:
- Сложность синхронизации слоёв: и это действительно так. Запустить слои одновременно - не проблема, проблема выстроить хорошо синхронизированный поток выполнения.
- Контроль потребления системных ресурсов: если слишком сильно забить память ресурсами, система может сильно замедлиться; попытка загружать по необходимости - раздражающее ожидание игроков. Короче говоря, нужна золотая середина.
- Нестандартное поведение: иногда порядок выполнения некоторых задач следует поменять или поведение по умолчанию заменить нестандартным. Это потребует частичного или полного переделывания одного из слоёв, либо же создание временного слоя для определённой игровой ситуации, что вряд ли можно назвать удобным подходом.
Это далеко не все проблемы, но лично для меня эти три уже несколько раз вставали на первое место.
И что со всем этим делать?
Однако, в идее слоёв есть одно феноменальное преимущество: их все можно разрабатывать параллельно, ограничиваясь, там где можно повременить, заглушками. То есть, можно разрабатывать новый движок, имея все слои одновременно, но писать код для них лишь по мере необходимости (проще говоря, когда дольше ждать нельзя).
Второй момент, на который надо обратить внимание, это разделяемые ресурсы; проще говоря ресурсы, которые используются и внутри слоёв движка, и внутри пользовательского слоя.
И третье, на что надо обратить внимание, это возможность подмены слоёв игрового движка в зависимости от обстоятельств; хотя есть слои, которые меняться, скорее всего, не будут.
Выводы
Я довольно много писал про архитектуру игр и игровые движки. Увы, чем больше набиваешь шишек, тем больше приходит понимания, как всё это должно работать.
Попытка разделить приложение на слои появилась тогда, когда стало ясно, что без этого приложение превращается в "кашу": внутри одного объекта содержится слишком много данных, в том числе не профильных. С другой стороны, другие части приложения становятся слишком раздробленными в интересах повышения абстракций.
Даже попытка прочитать абзац выше создаёт ощущение хаоса, а представьте себе, что в коде творится.
Движок нужен. Чем больше он фрагментирован - тем удобней разрабатывать и меньше энтропия, однако это может стоить очень дорого на мержфрагментное взаимодействие. Надеюсь, что предложенная концепция 7 слоёв решит данную проблему.
По мере разработки игры я сам загнал себя в некоторое количество ловушек, которые сильно затруднили разработку. Поэтому теперь есть идея попробовать добавить систему слоёв и посмотреть, что из этого выйдет.