Скрипты в 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’ов и доступны из кода всегда, даже если игрок сейчас в другой локации), назначать этим сущностям задачи в очередь выполнения и подписываться на их эвенты.
Система эвентов представляет собой надстройку на основе делегатов. Можно подписываться, как на события от конкретной сущности, так и глобально на все события определённого типа. Во втором случае можно также добавить подписке фильтры. Например, подписаться на клики по объектам с определённым тегом. Обработчик события при сохранении резолвится в имя скрипта и метода. Да, для совместимости сейвов переименовывать методы нельзя. Во всём остальном проекте их можно переименовывать, потому что они помечаются атрибутами сериализации; но в скриптах решили атрибуцию не делать, чтобы их не усложнять (скриптеры готовы мириться с таким ограничением, а если уж очень надо, то рулить версионность ифами). Также в классе скрипта сериализуются и попадают в сейв все его поля. Для разработчика скрипта это получаются своего рода локальные переменные, где он может хранить всякие временные штуки.
Вот такая примерно система у нас. На мой взгляд, получилось довольно гибко и удобно. Так ли это на самом деле покажет время, когда мы перейдём в фазу активного наполнения игры контентом (пока у нас основная механика и даже архитектура ещё в процессе). Буду держать вас в курсе.
#код
Формально весь код, который мы пишем для 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’ов и доступны из кода всегда, даже если игрок сейчас в другой локации), назначать этим сущностям задачи в очередь выполнения и подписываться на их эвенты.
Система эвентов представляет собой надстройку на основе делегатов. Можно подписываться, как на события от конкретной сущности, так и глобально на все события определённого типа. Во втором случае можно также добавить подписке фильтры. Например, подписаться на клики по объектам с определённым тегом. Обработчик события при сохранении резолвится в имя скрипта и метода. Да, для совместимости сейвов переименовывать методы нельзя. Во всём остальном проекте их можно переименовывать, потому что они помечаются атрибутами сериализации; но в скриптах решили атрибуцию не делать, чтобы их не усложнять (скриптеры готовы мириться с таким ограничением, а если уж очень надо, то рулить версионность ифами). Также в классе скрипта сериализуются и попадают в сейв все его поля. Для разработчика скрипта это получаются своего рода локальные переменные, где он может хранить всякие временные штуки.
Вот такая примерно система у нас. На мой взгляд, получилось довольно гибко и удобно. Так ли это на самом деле покажет время, когда мы перейдём в фазу активного наполнения игры контентом (пока у нас основная механика и даже архитектура ещё в процессе). Буду держать вас в курсе.