Как пишут моды для игр

В этой части на примере мода для Beat Saber мы рассмотрим общие принципы разработки модов для Unity-игр, узнаем, какие есть трудности, а также познакомимся с Harmony — библиотекой для модификации кода игр, которая используется в RimWorld, Battletech, Cities: Skylines и многих других играх.

Хоть эта статья и похожа на туториал, как написать свой мод для Beat Saber, ее цель — показать, какие принципы используются при создании любых пользовательских модов и какие проблемы приходится решать при разработке. Все, что здесь описано, с некоторыми оговорками применимо для всех Unity-игр как минимум в Windows.

Источники изображений: 1, 2

В предыдущей серии

Прошлая часть

Информация из первой части не нужна для понимания того, что будет происходить здесь, но все равно советую с ней ознакомиться.

Вот ее краткое (очень) содержание:

Программные моды (также известные как плагины) — это dll-библиотеки, которые загружаются вместе с игрой и выполняют какой-то код, добавляя в игру новую функциональность или модифицируя существующую. Если у игры нет встроенной поддержки модов, то никакие dll-файлы она запускать не будет. Поэтому для внедрения сторонних модов используются специальные библиотеки, например BepInEx или IPA. В Beat Saber используется BSIPA — улучшенная версия IPA. Сначала ее просто адаптировали специально для Beat Saber, а сейчас она в техническом плане значительно превосходит оригинальную IPA и может использоваться для любых Unity-игр.

Про Beat Saber и мод, который мы будем делать

Beat Saber является одной из самых популярных игр для VR-шлемов. Если у вас есть такой шлем, то, скорее всего, вы уже знаете, что такое Beat Saber. Если нет, то, возможно, вы видели хотя бы одно видео из игры в рекомендациях Youtube:

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

В этой статье будет описана полная разработка мода, начиная с создания пустого проекта. Я разбил все на 5 шагов, в конце каждого шага будет краткий вывод об особенностях разработки модов. Если не хотите углубляться в код и детали, то можно просто пробежаться по выводам. Для полного понимания желательно знать основы Unity: работа со сценами, иерархия объектов, компоненты и их жизненный цикл.

Подготовка

Для начала нам нужно сделать так, чтобы игра была пригодна для модов. Для этого в случае с Beat Saber нужно скачать ModAssistant, настроить его (ничего сложного), установить обязательные моды вроде BSIPA, SongCore и BS_Utils и установить другие моды по вкусу. Теперь игра поддерживает моды, а в папках с игрой есть все нужные для нас библиотеки, и можно приступать к разработке.

В случае с другими играми нужно либо искать, что используется у них, либо читать мою прошлую статью про моды и добавлять поддержку модов самостоятельно.

Замечание про версии

Все, что написано в данной статье, работает как минимум для Beat Saber версии 1.9.1 и BSIPA версии 4.0.5. Все развивается и меняется, поэтому если вы читаете этот текст спустя какое-то время после его публикации, то имейте в виду, что часть информации может устареть.

Шаг 0: минимальный рабочий мод

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

Начальные шаги неплохо написаны на сайте Beat Saber Modding Group (далее просто BSMG). К сожалению, только начальные шаги там и описаны. Там предлагается несколько шаблонов Visual Studio для создания проекта на выбор — просто берете, какой нравится и создаете проект из шаблона.

В этой статье мы пойдем более трудным путем и создадим проект с нуля. Берем любимую среду разработки для C# (у меня Rider), создаем новый C#-проект, выбираем Class Library в качестве целевой сборки и выбираем версию .NET, совместимую с Unity (у меня 4.7.2). Получаем пустой проект. Теперь создаем файлы мода.

manifest.json

Json-файл, содержащий мета-данные для BSIPA. Помечаем его в проекте как EmbeddedResource, чтобы при сборке он добавлялся внутрь нашего dll-файла.

{
  "$schema": "https://github.com/beat-saber-modding-group/BSIPA-MetadataFileSchema/blob/master/Schema.json",
  "author": "fck_r_sns",
  "description": "A mod to track active time spent in the game",
  "gameVersion": "1.8.0",
  "id": "BeatSaberTimeTracker",
  "name": "BeatSaberTimeTracker",
  "version": "0.0.1-alpha",
  "dependsOn": {}
}

$schema указывает на файл с описанием схемы для валидации формата. Файл лежит на GitHub в репозитории BSIPA. Нас это сильно волновать не должно, просто добавляем и забываем. В dependsOn указываем, какие сторонние моды мы используем в нашем собственном моде. BSIPA использует эту информацию, чтобы определить порядок загрузки dll-файлов. gameVersion и version используют семантическое версионирование.

Plugin.cs

Теперь создаем класс, который будет точкой входа для нашего плагина. В BSIPA 3 нужно было написать класс, реализующий интерфейс IBeatSaberPlugin. BSIPA 3 считывала все классы из dll-файла мода, находила там класс, реализующий интерфейс IBeatSaberPlugin, и создавала объект этого класса — так запускался мод. В BSIPA 4 убрали интерфейс IBeatSaberPlugin. Теперь BSIPA ищет класс, помеченный атрибутом [Plugin], и методы с атрибутами [Init], [OnStart] и [OnExit].

using IPA;
using Logger = IPA.Logging.Logger;

namespace BeatSaberTimeTracker
{
    [Plugin(RuntimeOptions.SingleStartInit)]
    internal class Plugin
    {
        public static Logger logger { get; private set; }

        [Init]
        public Plugin(Logger logger)
        {
            Plugin.logger = logger;
            logger.Debug("Init");
        }

        [OnStart]
        public void OnStart()
        {
            logger.Debug("OnStart");
        }

        [OnExit]
        public void OnExit()
        {
            logger.Debug("OnExit");
        }
    }
}

Название класса может быть любое, но обычно его просто называют Plugin. Главное, чтобы пространство имен (namespace) соответствовало названию, которое мы указали в манифесте — в данном случае это BeatSaberTimeTracker. На этом этапе мы просто будем писать в лог, если был вызван какой-то метод.

Чтобы это собралось, нужно указать компилятору, где определены атрибуты [Plugin], [Init], [OnStart] и [OnExit]. Для этого в свойствах проекта добавляем в зависимости файл IPA.Loader.dll. Будем считать, что моды у нас уже внедрены в игру, а значит, все нужные библиотеки уже лежат в папке с Beat Saber где-то в папках Steam. Библиотеки игры, Unity, системные библиотеки и файлы IPA лежат в папке Beat Saber/Beat Saber_Data/Managed. Все просто добавляют файлы прямиком из папки Steam в проект и так и выкладывают на GitHub, тут нечего стесняться. BSMG сами советуют так делать.

Собираем наш мод, копируем получившийся dll-файл в папку Beat Saber/Plugins и запускаем игру. Для простой отладки не обязательно подключать VR-шлем, можно запустить игру из терминала с флагом fpfc. Игра запустится в режиме отладки с управлением мышью. Этого достаточно, чтобы потыкать кнопки в главном меню. После этого выходим из игры, идем в папку Beat Saber/Logs и ищем там логи для нашего мода.

[DEBUG @ 20:50:03 | BeatSaberTimeTracker] Init
[DEBUG @ 20:50:03 | BeatSaberTimeTracker] OnStart
[DEBUG @ 20:50:21 | BeatSaberTimeTracker] OnExit

Поздравляю, наш мод работает.

Вывод для шага 0

У любого мода должна быть точка входа. Это что-то типа аналога main в обычных программах. Детали реализации зависят от того, как именно работают моды: где-то нужно реализовать интерфейс, где-то использовать атрибуты или аннотации, а где-то просто добавить метод с определенным именем.

Полный код текущего этапа

Шаг 1: выводим время на экран

На этом шаге сделаем так, чтобы мод делал что-то осмысленное, но еще не трогал код самой игры — добавим часы где-нибудь в углу и покажем время, проведенное в игре с ее запуска. Последуем принципу единственной ответственности и создадим новый класс TimeTracker. Класс Plugin нужен только для запуска и инициализации мода, никакой другой логики там быть не должно.

На этом этапе класс TimeTracker будет создавать canvas в мировом пространстве, добавлять на него два текстовых поля и раз в секунду обновлять на них значения.

Создаем объекты в Awake:

private void Awake()
{
    Plugin.logger.Debug("TimeTracker.Awake()");

    GameObject canvasGo = new GameObject("Canvas");
    canvasGo.transform.parent = transform;
    _canvas = canvasGo.AddComponent<Canvas>();
    _canvas.renderMode = RenderMode.WorldSpace;

    var canvasTransform = _canvas.transform;
    canvasTransform.position = new Vector3(-1f, 3.05f, 2.5f);
    canvasTransform.localScale = Vector3.one;

    _currentTimeText = CreateText(_canvas, new Vector2(0f, 0f), "");
    _totalTimeText = CreateText(_canvas, new Vector2(0f, -0.15f), "");
}

Создаем объект, добавляем на него Canvas, настраиваем его, создаем два текстовых поля. Текстовые поля создаются в CreateText:

private static TextMeshProUGUI CreateText(Canvas canvas, Vector2 position, string text)
{
    GameObject gameObject = new GameObject("CustomUIText");
    gameObject.SetActive(false);
    TextMeshProUGUI textMeshProUgui = gameObject.AddComponent<TextMeshProUGUI>();

    textMeshProUgui.rectTransform.SetParent(canvas.transform, false);
    textMeshProUgui.rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
    textMeshProUgui.rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
    textMeshProUgui.rectTransform.sizeDelta = new Vector2(1f, 1f);
    textMeshProUgui.rectTransform.transform.localPosition = Vector3.zero;
    textMeshProUgui.rectTransform.anchoredPosition = position;

    textMeshProUgui.text = text;
    textMeshProUgui.fontSize = 0.15f;
    textMeshProUgui.color = Color.white;
    textMeshProUgui.alignment = TextAlignmentOptions.Left;
    gameObject.SetActive(true);

    return textMeshProUgui;
}

Этот метод выглядит громоздко, но, по сути, мы здесь просто создаем объект TextMeshProUGUI и выставляем параметры RectTransform, которые мы в обычном случае установили бы в редакторе Unity.

Тут мы подходим к одному серьезному ограничению при разработке модов для Unity-игр — у нас нет редактора Unity. У нас нет удобного графического интерфейса, и у нас нет сцены, на которой можно накидать все руками и сохранить в префаб — все нужно делать руками из кода. Из-за этого координаты объектов приходится подбирать экспериментально: пробуем какое-нибудь число, запускаем игру, смотрим в каком месте оказался текст. Меняем координаты, перезапускаем игру, смотрим. Повторять, пока текст не окажется там, где нужно.

Чтобы хотя бы примерно понимать, какие координаты должны быть у элементов интерфейса, я сначала вывел на экран 400 текстовых полей: сетку 20 на 20. В каждом поле я выводил его координаты. Это помогло мне начать хоть как-то ориентироваться в координатах и масштабе сцены.

В Update обновляем значения на текстовых полях:

private void Update()
{
    if (Time.time >= _nextTextUpdate)
    {
        _currentTimeText.text = DateTime.Now.ToString("HH:mm");
        _totalTimeText.text = $"Total: {Mathf.FloorToInt(Time.time / 60f):00}:{Mathf.FloorToInt(Time.time % 60f):00}";
        _nextTextUpdate += TEXT_UPDATE_PERIOD;
    }
}

Теперь обновляем наш класс Plugin, чтобы он создавал объект TimeTracker:

[OnStart]
public void OnStart()
{
    logger.Debug("OnStart");

    GameObject timeTrackerGo = new GameObject("TimeTracker");
    timeTrackerGo.AddComponent<TimeTracker>();
    Object.DontDestroyOnLoad(timeTrackerGo);
}

Чтобы наш объект жил долго и счастливо и не был убит сборщиком мусора, нужно либо прикрепить его к какой-нибудь существующей сцене в игре, либо вызвать DontDestroyOnLoad(…). Второй способ проще.

Чтобы все это работало, нам нужно добавить библиотеки Unity в список зависимостей проекта: UnityEngine.CoreModule.dll для GameObject и MonoBehaviour, UnityEngine.UI.dll и Unity.TextMeshPro.dll для TextMeshPro и UnityEngine.UIModule.dll для Canvas. Взять их можно все там же, в папке с игрой.

Собираем dll-файл, копируем его в папку с плагинами, запускаем игру и любуемся результатом.

Смотрим логи:

[DEBUG @ 21:37:18 | BeatSaberTimeTracker] Init
[DEBUG @ 21:37:18 | BeatSaberTimeTracker] OnStart
[DEBUG @ 21:37:18 | BeatSaberTimeTracker] TimeTracker.Awake()
[DEBUG @ 21:37:24 | BeatSaberTimeTracker] OnExit
[DEBUG @ 21:37:25 | BeatSaberTimeTracker] TimeTracker.OnDestroy()

Все отлично, наш мод работает и уже даже приносит пользу. Пока что он живет своей жизнью — он не влияет на игру, а игра не влияет на него. Но из-за этого у нашего мода есть серьезная проблема: он показывает время всегда, даже если оно нам мешает. Например, в самом геймплее. С этим мы разберемся далее.

Вывод из шага 1

У нас нет исходных файлов игры, а значит, ее нельзя открыть в редакторе Unity и пользоваться теми же инструментами, что и при нормальной разработке. Приходится изучать, как все устроено, выводя информацию либо в логи, либо через UI в самой игре.

Полный код текущего этапа

Дифф с прошлым этапом

Шаг 2: взаимодействуем с логикой самой игры

На этом шаге начинаем контактировать с игрой. Будем считать активное время, проведенное в геймплее, и прятать UI мода, когда он не нужен. Для этого нужно научиться определять переходы из меню в основной геймплей и определять, поставили ли игру на паузу.

Обновляем метод Update. Теперь будем использовать логическую переменную _trackActiveTime, чтобы включать и выключать отслеживание активного времени. Ну и выводим его в новое текстовое поле _activeTimeText. Создаем его так же, как и остальные, просто сдвигаем координаты чуть пониже.

private void Update()
{
    if (_trackActiveTime)
    {
        _activeTime += Time.deltaTime;
    }

    if (Time.time >= _nextTextUpdate)
    {
        _currentTimeText.text = DateTime.Now.ToString("HH:mm");
        _totalTimeText.text = $"Total: {Mathf.FloorToInt(Time.time / 60f):00}:{Mathf.FloorToInt(Time.time % 60f):00}";
        _activeTimeText.text = $"Active: {Mathf.FloorToInt(_activeTime / 60f):00}:{Mathf.FloorToInt(_activeTime % 60f):00}";
        _nextTextUpdate += TEXT_UPDATE_PERIOD;
    }
}

Теперь добавляем метод для включения и выключения отслеживания активного времени:

private void SetTrackingMode(bool isTracking)
{
    _trackActiveTime = isTracking;
    _canvas.gameObject.SetActive(!isTracking);
}

Здесь мы устанавливаем _trackActiveTime и скрываем текстовые поля. Это заодно решает проблему из прошлого этапа, когда время показывалось в основном геймплее.

Теперь нам нужно каким-то образом сделать так, чтобы основная игра вызывала SetTrackingMode(true), когда мы запускаем какой-то уровень, и SetTrackingMode(false), когда мы возвращаемся в меню или ставим игру на паузу. Проще всего это сделать через события. Для начала пойдем простым путем и добавим мод, который упрощает взаимодействие с игрой, а потом уже посмотрим, как это делается руками.

Нам нужен мод BS_Utils. Добавляем в список зависимостей проекта библиотеку BS_Utils.dll из папки Beat Saber/Plugins (мы ее установили когда ставили моды через ModAssistant). Теперь добавляем BS_Utils в манифест. Это нужно для того, чтобы наш мод загружался после него.

"dependsOn": {
    "BS Utils": "^1.4.0"
  },

Находим в событиях BS_Utils те, которые нам нужны, подписываемся на них и переключаем отслеживание активного времени.

BSEvents.gameSceneActive += EnableTrackingMode;
BSEvents.menuSceneActive += DisableTrackingMode;
BSEvents.songPaused += DisableTrackingMode;
BSEvents.songUnpaused += EnableTrackingMode;

Методы EnableTrackingMode и DisableTrackingMode я добавил просто для удобства, чтобы можно было их использовать как делегаты в событиях без аргументов.

private void EnableTrackingMode()
{
    SetTrackingMode(true);
}

private void DisableTrackingMode()
{
    SetTrackingMode(false);
}

Собираем проект, копируем dll в Plugins, запускаем игру, проверяем.



Если бы мы просто разрабатывали мод для Beat Saber, то на этом этапе можно было бы и остановиться. Мод готов, он делает то, что мы хотели, и так, как мы хотели. Он использует сторонний мод BS_Utils, но почти все моды используют его. BS_Utils поддерживается одним из главных разработчиков в сообществе BSMG, так что не нужно переживать, что в какой-то момент он перестанет работать. Но это познавательная статья, поэтому мы пойдем дальше. И мы еще не все разобрали, что нужно для разработки модов.

Вывод из шага 2

Если у игры большое сообщество моддеров, то, скорее всего, они уже сделали многое, чтобы облегчить работу друг другу. Например, в Beat Saber мод BS_Utils значительно упрощает работу с кодом игры, а BSML — это мод, позволяющий создавать графический интерфейс с помощью xml-конфигураций.

Полный код текущего этапа

Дифф с прошлым этапом

Шаг 3: удаляем BS_Utils, лезем в код игры

Удаляем BS_Utils из зависимостей проекта и из манифеста. Компилятор сообщает нам, что BSEvents и его события теперь не определены. Их мы и будем заменять на этом шаге.

menuSceneActive и gameSceneActive

Эти события срабатывают, когда активируется сцена с меню и сцена с основным геймплеем соответственно. Для работы со сценами у Unity есть статический класс SceneManager, у которого есть события sceneLoaded, sceneUnloaded и activeSceneChanged. Добавляем обработчики событий для них и просто выводим названия сцен в логи. Так как мы уже добавили библиотеку UnityEngine.CoreModule.dll в зависимости, проблем с определением SceneManager быть не должно.

private void Awake()
{
    ...
    SceneManager.sceneLoaded += OnSceneLoaded;
    SceneManager.sceneUnloaded += OnSceneUnloaded;
    SceneManager.activeSceneChanged += OnActiveSceneChanged;
    ...
}

private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
    Plugin.logger.Debug("OnSceneLoaded: " + scene.name + " (" + mode + ")");
}

private void OnSceneUnloaded(Scene scene)
{
    Plugin.logger.Debug("OnSceneUnloaded: " + scene.name);
}

private void OnActiveSceneChanged(Scene previous, Scene current)
{
    Plugin.logger.Debug("OnActiveSceneChanged: " + previous.name + " -> " + current.name);
}

Собираем мод, запускаем игру, заходим в основной геймплей, выходим из него, выходим из игры, смотрим логи.

[DEBUG @ 14:28:14 | BeatSaberTimeTracker] Plugin.Init
[DEBUG @ 14:28:14 | BeatSaberTimeTracker] Plugin.OnStart
[DEBUG @ 14:28:14 | BeatSaberTimeTracker] TimeTracker.Awake()
[DEBUG @ 14:28:15 | BeatSaberTimeTracker] OnSceneLoaded: EmptyTransition (Additive)
[DEBUG @ 14:28:15 | BeatSaberTimeTracker] OnActiveSceneChanged: PCInit -> EmptyTransition
[DEBUG @ 14:28:15 | BeatSaberTimeTracker] OnSceneLoaded: MainMenu (Additive)
[DEBUG @ 14:28:15 | BeatSaberTimeTracker] OnSceneLoaded: MenuCore (Additive)
[DEBUG @ 14:28:15 | BeatSaberTimeTracker] OnSceneLoaded: MenuEnvironment (Additive)
[DEBUG @ 14:28:15 | BeatSaberTimeTracker] OnSceneLoaded: MenuViewControllers (Additive)
[DEBUG @ 14:28:15 | BeatSaberTimeTracker] OnActiveSceneChanged: EmptyTransition -> MenuViewControllers
[DEBUG @ 14:28:15 | BeatSaberTimeTracker] OnSceneUnloaded: EmptyTransition
[DEBUG @ 14:28:22 | BeatSaberTimeTracker] OnSceneLoaded: BigMirrorEnvironment (Additive)
[DEBUG @ 14:28:22 | BeatSaberTimeTracker] OnSceneLoaded: StandardGameplay (Additive)
[DEBUG @ 14:28:23 | BeatSaberTimeTracker] OnSceneLoaded: GameplayCore (Additive)
[DEBUG @ 14:28:23 | BeatSaberTimeTracker] OnSceneLoaded: GameCore (Additive)
[DEBUG @ 14:28:23 | BeatSaberTimeTracker] OnActiveSceneChanged: MenuViewControllers -> GameCore
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnActiveSceneChanged: GameCore -> MenuViewControllers
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnActiveSceneChanged: MenuViewControllers -> MainMenu
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnActiveSceneChanged: MainMenu -> MenuCore
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnActiveSceneChanged: MenuCore -> MenuEnvironment
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnActiveSceneChanged: MenuEnvironment -> MenuViewControllers
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnSceneUnloaded: BigMirrorEnvironment
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnSceneUnloaded: StandardGameplay
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnSceneUnloaded: GameplayCore
[DEBUG @ 14:28:29 | BeatSaberTimeTracker] OnSceneUnloaded: GameCore
[DEBUG @ 14:28:34 | BeatSaberTimeTracker] Plugin.OnExit
[DEBUG @ 14:28:34 | BeatSaberTimeTracker] TimeTracker.OnDestroy()

Здесь так много разных сцен, потому что Beat Saber использует разные сцены для разных компонентов и загружает их в режиме Additive. Интерфейс на одной сцене, платформа с игроком — на другой. Анализируем логи и делаем вывод: отслеживать переход в основной геймплей можно, например, при активации сцены GameCore. По аналогии, переход в меню — по активации сцены MenuCore. Но с MenuCore есть проблема — судя по логам, она не активируется при запуске игры, когда мы только попадаем в меню. Поэтому для меню лучше использовать сцену MenuViewControllers. Еще одно полезное наблюдение: сцены для меню загружаются один раз при запуске игры и просто деактивируются при запуске геймплея, а вот сцены геймплея загружаются заново при запуске уровня. Это нам еще пригодится.

Обновляем OnActiveSceneChanged: проверяем имя сцены и переключаем отслеживание активного времени:

private void OnActiveSceneChanged(Scene previous, Scene current)
{
    Plugin.logger.Debug("OnActiveSceneChanged: " + previous.name + " -> " + current.name);
    switch (current.name)
    {
        case "MenuViewControllers":
            DisableTrackingMode();
            break;

        case "GameCore":
            EnableTrackingMode();
            break;
    }
}

songPaused и songUnpaused

Для следующих событий придется покопаться в коде игры, поэтому переходим к настоящему реверс-инжинирингу. Теперь нам нужна библиотека, в которой содержится код Beat Saber. В папке «Beat Saber/Beat Saber_Data/Managed» лежат 2 библиотеки: Main.dll и MainAssembly.dll. Я сначала копался в MainAssembly.dll, из-за чего потратил 2 дня на отладку одного очень странного поведения. Оказалось, что по какой-то причине и Main.dll, и MainAssembly.dll содержат определения одних и тех же классов. Я использовал MainAssembly.dll, а в игре использовались классы из Main.dll. Возможно, какая-то ошибка при сборке билда у разработчиков игры.

Судя по тому, что я узнал и посмотрел в других модах, все, что нам нужно, лежит в библиотеке Main.dll. Нам нужно посмотреть ее содержимое, а для этого нужен декомпилятор. На сайте BSMG советуют использовать dnSpy. Я использую Rider в качестве среды разработки, и у него есть встроенный декомпилятор, поэтому про dnSpy ничего конкретного сказать не могу, не пользовался. Но, судя по описанию, вещь полезная — это не только декомпилятор, но еще и дебаггер, который может подключаться к Unity-процессам.

Дальше идет рутина: берем содержимое Main.dll и ищем класс, который делает то, что нам нужно. Это сложно, но по-другому никак. Разве что можно пойти в Discord-канал BSMG и спросить. Вам, скорее всего, ответят, потому что там много людей, которые уже когда-то декомпилировали Main.dll и что-то там искали (и нашли).

Рано или поздно мы найдем класс GamePause, который отвечает в игре за включение и выключение паузы. У него есть два метода: Pause и Resume. А еще у GamePause есть два события: didPauseEvent и didResumeEvent. Отлично, нам даже не пришлось делать что-то сложное, у GamePause уже есть события, на которые мы можем подписаться.

Значит, нам каким-то образом нужно получить ссылку на компонент GamePause. В Unity это можно сделать так:

Resources.FindObjectsOfTypeAll<GamePause>();

Этому методу все равно, на какой сцене компонент, что за объект и активен ли он. Если компонент создан, он будет найден. Но нужно как-то найти момент времени, когда этот компонент создан. Можно предположить, что он висит на каком-то объекте на одной из сцен в геймплее. Мы уже выяснили, что геймплейные сцены каждый раз создаются заново. У нас есть обработчики событий OnSceneLoaded и OnActiveSceneChanged, поэтому мы можем отловить там сцену GameCore и в этот момент попробовать получить ссылку на GamePause. Проблема в том, что он может создаваться динамически чуть позже, чем загружаются сцены, поэтому тут есть два варианта: поискать в игре событие, которое срабатывает после того, как GamePause создан (вряд ли такое есть), либо вызывать Resources.FindObjectsOfTypeAll каждый кадр, пока не найдем компонент. Например, через корутину:

IEnumerator InitGamePauseCallbacks()
{
    while (true)
    {
        GamePause[] comps = Resources.FindObjectsOfTypeAll<GamePause>();
        if (comps.Length > 0)
        {
            Plugin.logger.Debug("GamePause has been found");
            GamePause gamePause = comps[0];
            gamePause.didPauseEvent += DisableTrackingMode;
            gamePause.didResumeEvent += EnableTrackingMode;
            break;
        }

        Plugin.logger.Debug("GamePause not found, skip a frame");
        yield return null;
    }
}

Запускаем ее в OnActiveSceneChanged для сцены GameCore:

private void OnActiveSceneChanged(Scene previous, Scene current)
{
    Plugin.logger.Debug("OnActiveSceneChanged: " + previous.name + " -> " + current.name);
    switch (current.name)
    {
        case "MenuViewControllers":
            DisableTrackingMode();
            break;

        case "GameCore":
            EnableTrackingMode();
            StartCoroutine(InitGamePauseCallbacks());
            break;
    }
}

Собираем мод, запускаем игру и убеждаемся, что все работает. Также можно заглянуть в логи. Там видно, что GamePause существует сразу же после активации GameCore, а значит, корутина не нужна и можно ее убрать. Я решил оставить для надежности.

Вывод из шага 3

Чтобы сделать мод для игры, нужно знать ее архитектуру и исходный код. Для этого приходится много времени тратить с декомпилятором, копаясь в исходном коде и пытаясь понять, как там все устроено. А копаться в чужом коде не всегда легко и приятно.

Полный код текущего этапа

Дифф с прошлым этапом

Шаг 4: вмешиваемся в логику игры с помощью Harmony

На этом этапе начинается магия, мы взглянем на Harmony — библиотеку для модификации C#-кода, которая используется моддерами во многих играх. Ее автор — Andreas Pardeike (сайт, GitHub), работает ведущим iOS-разработчиком / архитектором в шведской полиции (Swedish Police Authority). В отличие от библиотеки Mono.Cecil из прошлой статьи про моды, которая модифицирует и перезаписывает dll-файлы с .NET-сборками, Harmony модифицирует код во время исполнения программы (runtime). Модифицировать можно только методы, что обычно достаточно, так как нам нужно модифицировать именно поведение, а не состояние. Для модификации состояния есть много других способов, в том числе стандартных.

Модификации Harmony в терминах самой библиотеки называются патчами (patches). Есть несколько видов патчей:

  • Prefix. Патч, который вызывается перед выполнением метода. С его помощью можно перехватить и изменить аргументы метода, либо решить, нужно ли вызывать сам метод или сразу выйти из него.
  • Postfix. Патч, который вызывается после выполнения метода. Можно перехватить и изменить возвращаемое значение.
  • Transpiler. Патч, который на ходу модифицирует скомпилированный IL-код. Можно использовать, если нужно изменить логику где-то в середине метода.
  • Finalizer. С этим патчем мы как бы оборачиваем оригинальный метод в конструкцию try/catch/finally, а сам патч является обработчиком одновременно и catch, и finally.

Самые популярные патчи — это Prefix и Postfix. Transpiler слишком сложный, так как это уже не C#, а IL-код, да и зачастую проще скопировать исходный метод через декомпилятор, изменить там что-то и заменить весь метод через Prefix/Postfix. Finalizer звучит полезно, но он появился только недавно, в Harmony 2.0, поэтому примеров его использования я еще не видел.

Когда я только придумывал идею для мода, я думал, что Harmony мне понадобится сразу же, как только я решу убрать BS_Utils. Оказалось, что GamePause сам по себе содержит все нужные события, и теперь придется искусственно усложнить задачу, чтобы показать, как работает Harmony. Давайте представим, что в GamePause нет событий didPauseEvent и didResumeEvent, и нам нужно что-то с этим сделать.

Так как мы все еще придерживаемся принципа единственной ответственности, создаем класс HarmonyPatcher. У него будет всего один метод: public static void ApplyPatches() {}, в котором будет примерно такой код:

Harmony harmony = new Harmony("com.fck_r_sns.BeatSaberTimeTracker");
harmony.PatchAll(Assembly.GetExecutingAssembly());

Этих двух строк достаточно, чтобы установить все патчи, который у нас есть (но их пока нет). «com.fck_r_sns.BeatSaberTimeTracker» — это имя пакета. Оно должно быть уникальным, чтобы не было коллизий с патчами из других модов. Теперь идем в класс Plugin, который у нас отвечает за старт и инициализацию мода, и добавляем туда вызов HarmonyPatcher.ApplyPatches() перед созданием TimeTracker.

Переходим к написанию самих патчей. Для каждого метода, который мы хотим модифицировать, нужно написать отдельный класс. Каждый патч — это статический метод в этом классе. Чтобы указать, что это за патч, мы можем либо использовать соответствующее имя метода (например, метод с именем Prefix — это Prefix-патч), либо использовать любые имена и помечать методы атрибутами (например, [HarmonyPrefix]). Я всегда предпочитаю, чтобы код был явным и легко читаемым, поэтому я сторонник подхода с атрибутами. Начнем с патчей для метода GamePause.Pause(). Добавим в него Postfix-патч, который просто пишет в лог, что был вызван метод Pause() и сработал Postfix-патч.

[HarmonyPatch(typeof(GamePause), nameof(GamePause.Pause), MethodType.Normal)]
class GamePausePausePatch
{
    [HarmonyPostfix]
    static void TestPostfixPatch()
    {
        Plugin.logger.Debug("GamePause.Pause.TestPostfixPatch");
    }
}

Атрибут [HarmonyPatch] указывает, какие класс и метод нам нужно модифицировать. Статический метод TestPostfixPatch помечен атрибутом [HarmonyPostfix], поэтому это Postfix-патч. Создаем аналогичный класс для GamePause.Resume() (можно в том же файле), собираем, запускаем игру, запускаем уровень, жмем паузу, снимаем паузу, выходим из игры, проверяем логи.

Проверяем, что патчи применились:

[DEBUG @ 16:21:55 | BeatSaberTimeTracker] Plugin.Init
[DEBUG @ 16:21:55 | BeatSaberTimeTracker] Plugin.OnStart
[DEBUG @ 16:21:55 | BeatSaberTimeTracker] HarmonyPatcher: Applied
[DEBUG @ 16:21:55 | BeatSaberTimeTracker] TimeTracker.Awake()

Проверяем, что Postfix-патчи сработали:

[DEBUG @ 16:22:24 | BeatSaberTimeTracker] GamePause.Pause.TestPostfixPatch
[DEBUG @ 16:22:31 | BeatSaberTimeTracker] GamePause.Resume.TestPostfixPatch

Отлично, Harmony работает, можно переходить к логике. В нашем искусственном примере мы представили, что событий didPauseEvent и didResumeEvent не существует, а значит, нам нужно в Postfix-патчах что-то сделать, чтобы TimeTracker включал и выключал отслеживание активного времени. Тут мы натыкаемся на главную проблему Harmony — все патчи являются статическими методами. А TimeTracker — это компонент, который висит где-то в иерархии объектов и статическим явно не является. Тут я вижу два нормальных решения этой задачи.

Первый — это сделать TimeTracker доступным из статического контекста. Например, сделать его синглтоном или каждый раз получать на него ссылку через Resources.FindObjectsOfTypeAll(). В BS_Utils, например, используется синглтон.

Второй — это добавить класс со статическими событиями вроде BS_Utils.Utilities.BSEvents, который мы использовали на ранних этапах. Этот вариант мне нравится больше, давайте реализовывать его.

Создаем класс EventsHelper:

namespace BeatSaberTimeTracker
{
    public static class EventsHelper
    {
        public static event Action onGamePaused;
        public static event Action onGameResumed;
    }
}

Теперь обновляем наши патчи, чтобы они вызывали эти события:

[HarmonyPatch(typeof(GamePause), nameof(GamePause.Pause), MethodType.Normal)]
class GamePausePatchPause
{
    [HarmonyPostfix]
    static void FireOnGamePausedEvent()
    {
        EventsHelper.FireOnGamePausedEvent();
    }
}

GamePauseResumePatch делается аналогично. Пришлось добавить публичные методы FireOnGamePausedEvent и FireOnGameResumedEvent, так как нельзя вызывать события из-за пределов их класса. Теперь TimeTracker может в любой момент подписаться на события в EventsHelper. Получаем код со слабым зацеплением — именно из-за этого подход с событиями мне нравится больше, чем вариант с синглтоном или Resources.FindObjectsOfTypeAll().

Если мы соберем мод и запустим игру, то все будет работать. Однако, мы пока не учли одну деталь. В оригинальном коде GamePause.Pause() есть проверка от многократного перехода в режим паузы.

if (this._pause)
  return;
this._pause = true;
…

Postfix-патч же будет вызван в любом случае: и если мы установили паузу, и если это было повторное нажатие. А значит, и событие EventsHelper будет срабатывать всегда, даже если фактического перехода в паузу уже не было. Давайте добавим Prefix-патч, в котором будем проверять текущее состояние паузы. Harmony позволяет читать и изменять приватные переменные класса, а также передавать состояние между патчами одного метода. В Harmony вообще много чего можно получить в патче:

  • Аргументы метода: собственно то, что было передано в метод при его вызове.
  • __instance: ссылка на текущий объект, для которого вызван метод. По сути это просто this.
  • __state: переменная любого типа для передачи состояния между патчами. Если нужно несколько переменных, то просто пишем структуру или класс.
  • __result: возвращаемый результат оригинального метода. Если нужно, можно его изменить.
  • Приватные переменные: добавляем три (3) знака подчеркивания (_) перед названием аргумента в патче, и Harmony подставит туда значение из приватной переменной.

Начнем со структуры, которая будет хранить состояние:

struct PauseState
{
    public bool wasPaused;
}

Нам нужно всего одно значение, чтобы отслеживать состояние паузы, поэтому структура избыточна, но как я уже писал выше, я люблю ясный код. PauseState __state — это более ясный код, чем просто bool __state.

Теперь добавляем Prefix-патч:

[HarmonyPrefix]
static void CheckIfAlreadyPaused(out PauseState __state, bool ____pause)
{
    __state = new PauseState { wasPaused = ____pause };
}

Здесь мы добавляем состояние с модификатором out, чтобы его можно было изменять, и приватную переменную ____pause (_pause и еще три подчеркивания перед ней). Просто сохраняем ____pause в __state — тут ничего хитрого.

Теперь обновляем Postfx-патч:

[HarmonyPostfix]
static void FireOnGamePausedEvent(PauseState __state, bool ____pause)
{
    if (!__state.wasPaused && ____pause)
    {
        EventsHelper.FireOnGamePausedEvent();
    }
}

__state даст нам ту же структуру, которую мы записали в Prefix-патче. Сравниваем wasPaused с ____pause, чтобы проверить, что игра реально поставлена на паузу и вызываем событие.

Полный код патчей

Запускаем игру и проверяем, что все работает.

Вывод из шага 4

Harmony — это очень полезная и важная для сообщества моддеров библиотека, которая используется в RimWorld, Battletech, Cities: Skylines, Kerbal Space Program, Oxygen Not Included, Stardew Valley, Subnautica и многих других играх.

Полный код текущего этапа

Дифф с прошлым этапом

Заключение

Создание модов — это довольно утомительный процесс. При разработке модов нужно постоянно копаться в декомпилированном коде, искать классы, которые делают то, что вам нужно, модифицировать их, постоянно пересобирать моды, чтобы проверить изменения в игре, страдать из-за отсутствия нормального режима отладки и полноценного Unity-редактора.

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

Простым языком о dll-файлах, Unity, Mono, паре хакерских методов и о крутости open-source-сообщества.

Я довольно много играю в Beat Saber с модами, и в какой-то момент появилась необходимость написать свои. Beat Saber сделан на Unity, так что я покопался в его коде, в коде существующих модов, разобрался, как там всё устроено, и написал об этом лонгрид.

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

Моей задачей было показать, как глубоко может залезть инженерная мысль, чтобы добавить в игру что-то новое. Я старался объяснять всё, что может показаться сложным человеку далекому от программирования, но если почувствуете, что что-то совсем непонятно, не бойтесь задавать вопросы.

Источники изображений: 1, 2

О Beat Saber

Если у вас есть VR-шлем, то вы почти наверняка знаете, что такое Beat Saber. Если нет, то, вероятно, вы видели хотя бы одно видео из игры.

Единственное, что Beat Saber поддерживает из модов — это пользовательские уровни. В игре есть официальный редактор, но нет никакого сервиса, чтобы как-либо распространять свои творения (даже мастерской Steam), и вряд ли появится. В основе практически любого уровня лежит какая-то песня. Разрешить выкладывать такие файлы в своем официальном сервисе — значит привлечь к себе внимание правообладателей с их постоянными запросами что-то удалить, а то и вообще с требованиями всё закрыть.

Поэтому не удивительно, что вокруг Beat Saber сформировалось огромное сообщество: Beat Saber Modding Group (BSMG). Именно сообщество ответственно за большую часть того, что есть в игре.

  • https://beatsaver.com — коллекция пользовательских уровней, созданных с помощью официального или неофициальных редакторов. Сюда добавляются десятки уровней каждый день.
  • https://bsaber.com — сайт, главная задача которого — помочь найти что-то хорошее среди того, что появляется на https://beatsaver.com. Здесь есть рекомендации кураторов, топы за неделю, поиск по музыкальным жанрам и многое другое.
  • https://scoresaber.com — таблицы лидеров для пользовательских уровней.
  • https://modelsaber.com — модели мечей, платформ и аватаров. Тут хранится визуальный 3D-контент, не содержащий дополнительного кода.
  • https://beatmods.com — коллекция модов, модифицирующих код игры.
  • https://github.com/Assistant/ModAssistant — ModAssistant, программа для установки модов. Игрок просто выбирает моды, которые ему нужны, и жмет Install. Не нужно даже вручную копировать файлы.

Всё это поддерживается сообществом. Большинство модов и даже сами сайты — это проекты с открытым исходным кодом, их можно найти на GitHub — сайте, где программисты выкладывают свой код. Этим мы и воспользуемся дальше.

Немного информатики

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

Исполняемый файл (executable) — это файл, который содержит код в понятном для компьютера виде — в виде набора инструкций процессора. В большинстве случаев в Windows это файлы с расширением .exe. Исполняемые файлы, внезапно, могут исполняться — у них есть точка входа (entry point) и набор подпрограмм (они же процедуры, функции, методы).

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

Файлы .dll — это файлы динамических библиотек (dynamic link library). По своей сути и структуре они очень похожи на исполняемые файлы, но у них нет точки входа. Это просто наборы подпрограмм, которые загружаются в память вместе с exe-файлом и могут использоваться процессором.

У таких библиотек есть много преимуществ. Одну такую библиотеку могут использовать несколько программ одновременно: достаточно один раз загрузить её в память, и она станет доступна сразу для всех. Или при определённых условиях программе может вообще не понадобиться какая-то библиотека, и тогда её можно не загружать в память (потому они и называются «динамические»).

Также такие библиотеки могут поставляться другими разработчиками. Если вы когда-нибудь при запуске игры видели сообщение, что вам нужно дополнительно установить Microsoft Visual C++ Redistributable 2015, то знайте — это как раз оно и есть.

Помимо простых .dll существуют ещё так называемые сборки с управляемым кодом .NET (managed .NET assemblies). Это тоже dll-файлы, но они содержат не команды процессора, а команды промежуточного языка (Common Intermediate Language, CIL или иногда просто IL).

Процессоры не знают, что такое CIL, они не могут выполнять такие инструкции сами по себе. Для этого Windows запускает отдельную программу, которая на ходу переводит CIL-команды в инструкции, понятные процессору. Такой процесс называется компиляцией на ходу (just-in-time compilation, JIT), а программа, которая это делает — «общеязыковая исполняющая среда» (Common Language Runtime, CLR). Есть много языков, программы на которых собираются в такие сборки, но самым известным и популярным является C# (именно он используется в Unity).

Есть разные реализации Common Language Runtime. По сути это просто стандарт, и кто угодно может написать свою версию. Популярных версий всего три:

  • .NET Framework — основная платформа, с неё и пошли все эти C#, CIL и CLR. Разрабатывается Microsoft c 1999 года, работает под Windows. Считается, что .NET Framework появился как ответ Java, набирающей популярность в то время.
  • .NET Core — платформа с открытым исходным кодом, тоже разрабатывается Microsoft, но уже под разные платформы: Windows, Linux и macOS. Разрабатывается с 2014 года. Судя по всему, является попыткой Microsoft охватить серверы, работающие на Linux.
  • Mono — ещё одна платформа с открытым исходным кодом, работает на Windows, Linux, macOS, мобилках и консолях. Используется в Unity. Разрабатывается с 2001 года компанией Ximian, которую в 2003 купила компания Novell, которую в 2011 купила Attachmate. Attachmate поуволняла всех, но разработчики Mono хотели и дальше разрабатывать Mono, поэтому организовали в том же году отдельную компанию Xamarin, которую в 2016 купили Microsoft.

Таким образом, все основные реализации CLR сейчас находятся под крылом Microsoft. Однако некоторое время назад титул «корпорации добра» перешел от Google к Microsoft, так что не факт, что это плохо.

На этом с ликбезом пока достаточно, пора переходить к основной теме. Программные моды (также известные как плагины) — это dll-файлы, которые добавляются к игре и загружаются в память вместе с ней. Проблема в том, что Beat Saber не поддерживает сторонние плагины и сам по себе ничего загружать и использовать не собирается. Значит, нам нужно что-то, что внедрится в код игры и научит её это делать. Этим чем-то является BSIPA.

BSIPA

BSIPA (Beat Saber Illusion Plugin Architecture) — это набор библиотек, которые модифицируют файлы Beat Saber так, чтобы игра могла загружать сторонние моды. Иногда такие библиотеки называют менеджерами плагинов.

Небольшое лирическое отступление. Для того, чтобы мне было проще объяснять некоторые вещи, мне нужно объяснить значение слова «форк».

Разработка программ с открытым исходным кодом (open source) — это явление, которое оказывает колоссальный эффект на развитие технологий. Разработчики пишут программы, выкладывают их исходный код со свободной лицензией, например, на GitHub, а другие разработчики могут свободно использовать эти программы, предлагать определённые улучшения для существующих программ (пул реквесты, pull requests) или вообще копировать другие проекты и делать что-то свое на их основе. Последнее как раз и называется «форком» (fork, переводится буквально как «вилка», «развилка», «ответвление»).

Форк

Так вот, BSIPA (написана на C#, исходный код: GitHub) является форком IPA (GitHub). Т.е. моддеры взяли уже существующий менеджер плагинов для Unity и улучшили его специально под нужды Beat Saber. Давайте залезем в исходный код, чтобы понять, как оно всё работает. Я выделяю там три основных модуля: IPA, IPA.Loader и IPA.Injector. На самом деле их больше, но остальные не так важны.

IPA.exe

Обычно игроки используют ModAssistant или его аналоги, чтобы устанавливать моды, но мы будем делать это вручную. Для этого нужно скачать последний релиз BSIPA, распаковать все файлы в папку с игрой и запустить IPA.exe. Это исполняемый файл, который копирует файлы из только что распакованного архива в те места, где они должны находиться. И это, в общем-то, всё, что он делает: просто копирует файлы, и если какой-то из файлов уже существует в игре, то он делает его резервную копию. Давайте посмотрим на список файлов.

Beat Saber_DataManagedI18N.dll

Beat Saber_DataManagedI18N.West.dll

Beat Saber_DataManagedIPA.Injector.dll

Beat Saber_DataManagedIPA.Injector.pdb

Beat Saber_DataManagedIPA.Loader.dll

Beat Saber_DataManagedIPA.Loader.pdb

Beat Saber_DataManagedIPA.Loader.xml

Beat Saber_DataManagedMicrosoft.CSharp.dll

Beat Saber_DataManagedSystem.Runtime.Serialization.dll

LibsHarmony.1.2.0.1.dll

LibsIonic.Zip.1.9.1.8.dll

LibsMono.Cecil.0.10.4.0.dll

LibsMono.Cecil.Mdb.0.10.4.0.dll

LibsMono.Cecil.Pdb.0.10.4.0.dll

LibsMono.Cecil.Rocks.0.10.4.0.dll

LibsNewtonsoft.Json.12.0.0.0.dll

LibsSemVer.1.2.0.0.dll

winhttp.dll

Мы видим здесь ещё два модуля BSIPA, которые я упомянул ранее: IPA.Loader.dll и IPA.Injector.dll. Остальные библиотеки нужны, чтобы работали эти две. Некоторые из них мы ещё рассмотрим подробнее в этом лонге и во второй части.

IPA.Loader.dll

Как следует из названия этого модуля, он отвечает за загрузку плагинов и управление ими. Здесь определен класс PluginComponent — компонент Unity, отвечающий за хранение модов и передачу им событий из игры. Например, если в основной игре сменилась активная сцена (мы запустили какой-то уровень или, наоборот, вернулись в главное меню), то PluginComponent пробегается по всем модам и сообщает им об этом. Подробнее об этом будет во второй части.

IPA.Injector.dll

Название Injector намекает, что этот модуль отвечает за внедрение в оригинальную игру. Начнем с забавного факта: BSIPA добавляет в игру антипиратскую защиту.

if (AntiPiracy.IsInvalid(Environment.CurrentDirectory))
{
loader.Error(«Invalid installation; please buy the game to run BSIPA.»);
return;
}

Защита очень примитивная, никакого Denuvo, никакой виртуализации, она просто проверяет, есть ли в папке с игрой файлы, которые явно указывают на взлом: SmartSteamEmu.ini, BSteam crack.dll, huhuvr_steam_api64.dll и другие. Скорее всего, BSIPA это нужно из практических интересов. Внедрение в код игры и добавление туда модов — это по своей сути взлом. Если игра ещё и пиратская, то получается, что она взломана дважды, а совместная работа двух взломов не гарантируется.

В IPA.Injector используется сторонняя библиотека Mono.Cecil (исходный код: GitHub). Её автор: Jb Evain, когда-то работал в Novell над разработкой Mono, сейчас работает в Microsoft, руководит разработкой Visual Studio Tools for Unity. Mono.Cecil существует с 2004 года — примерно со времен релиза первой версии Mono.

Она позволяет читать и модернизировать .NET-сборки. То есть с её помощью можно менять различные параметры C#-библиотек или даже редактировать код, который в них записан. Здесь нет ничего особо сложного: Mono.Cecil редактирует файлы dll примерно как Microsoft Office редактирует файлы docx — нужно просто знать структуру файла, что как прочитать и что куда записать. Я не умаляю заслуг Jb Evain, если что. Библиотека всё ещё очень сложная и полезная, просто со стороны её изучать не так интересно.

И раз я уже не первый раз упомянул Mono в этом лонге, то надо написать про него чуть подробнее. Я уже писал в начале, что Mono — это одна из реализаций Common Language Runtime — программы, которая нужна, чтобы запускать код из управляемых сборок .NET. Если говорить проще и про наш конкретный случай, Mono нужен для того, чтобы в Unity можно было писать код на C#. Ядро движка Unity написано на C++, но некоторые части движка и практически все скрипты разрабатываемых на нем игр пишутся на C#. Когда мы запускаем игру, то сначала загружается C++ код. Этот код запускает Mono, передает в него C#-код движка и код игры, и после этого сама игра начинает работать — можно играть.

С помощью Mono.Cecil IPA.Injector редактирует библиотеку UnityEngine.CoreModule.dll. Как видно из названия, это одна из библиотек Unity. Она написана на C# (иначе бы мы не смогли её редактировать) и содержит базовые сущности движка. Например, там есть класс GameObject — все объекты в игре, которые располагаются на игровых сценах, являются объектами этого класса.

Ещё там есть класс MonoBehaviour — это класс для компонентов с игровой логикой (тут Mono даже в название класса затесался). IPA.Injector находит в библиотеке UnityEngine.CoreModule.dll класс UnityEngine.Application и модифицирует его статический конструктор (или создает, если его нет), добавляя туда код из IPA.Loader. Не буду углубляться в то, что такое статический конструктор, но теперь, когда загружается класс Application, у нас создаётся PluginComponent и загружаются плагины из папки Plugins. Получается, что мы внедрились не в код Beat Saber, чтобы он загружал моды, а в код самого Unity.

Если бы у нас использовалась не BSIPA, а оригинальная IPA, то на этом можно было бы и остановиться. В оригинальной версии IPA.exe запускает Injector, модифицирует UnityEngine.CoreModule.dll, и у нас появляются моды в игре. У такого подхода есть один минус — каждый раз, когда оригинальная игра обновляется, нужно заново запускать IPA.exe и патчить игру.

Это может показаться не такой серьёзной проблемой, но видели бы вы, сколько жалоб поступает от игроков к разработчикам игры за то, что они очередным своим обновлением сломали их моды. BSIPA решает эту проблему, но как я писал выше, IPA.exe в BSIPA просто копирует файлы и не делает больше ничего. Тогда непонятно, кто же запускает Injector в этом случае? И тут начинается самое интересное.

Unity Doorstop

Как оказалось, за внедрение в код Unity отвечает библиотека UnityDoorstop-BSIPA. Она лежит среди файлов BSIPA и написана на чистом C. UnityDoorstop-BSIPA (исходный код: GitHub) — это тоже форк, оригинальный проект можно найти здесь: GitHub. Далее для простоты буду вместо UnityDoorstop-BSIPA просто писать Doorstop. Лозунгом Doorstop является фраза «Run managed code before Unity does», что в примерном переводе звучит как «Запускай управляемый код до того, как Unity сможет это сделать».

Напомню, что «управляемый код» — это в нашем случае код C#. Выше мы уже выяснили, что ядро движка Unity написано на C++, а пользовательские скрипты для игровой логики и некоторые части самого Unity — на C#. Значит, Doorstop каким-то образом позволяет нам вмешаться в логику, когда ядро Unity уже загрузилось, а C#-скрипты — ещё нет.

Теперь уже лезем в исходники UnityDoorstop-BSIPA и смотрим, как это делается. Дальше идет немного более продвинутый уровень. Я постарался описать всё простыми словами, но при этом достаточно подробно, чтобы можно было оценить всю техническую глубину происходящей там магии.

Чтобы объяснить, как работает Doorstop, давайте вернёмся к нашим «основам информатики», которые уже перестают быть основами. Я уже писал выше, что исполняемые файлы могут использовать функции из других библиотек, переходя по адресам этих функций. Давайте уточним, откуда берутся эти адреса. В файлах .exe и .dll есть такие параметры как таблица импорта (Import Address Table, IAT) и таблица экспорта (Export Address Table, EAT). Импорт — это то, что требуется файлу из других библиотек, а экспорт — это то, что файл предоставляет для использования другим файлам. Очевидно, у .exe есть только импорт, потому что его никто использовать не может, а у .dll может быть и то, и другое, потому что они и сами что-то используют, и другим дают пользоваться.

Теперь вернемся к Unity и рассмотрим конкретный пример, который и подведет нас к тому, как работает Doorstop. Когда мы запускаем игру на Unity (например, Beat Saber.exe), то в память одной из первых загружается библиотека UnityPlayer.dll. Она прилагается ко всем Unity-играм и отвечает за запуск и выполнение самой игры. Всё указывает на то, что там как раз и есть код движка, написанный на C++, но я не проверял.

У этой библиотеки есть таблица импорта, в которой говорится, что UnityPlayer использует функцию GetProcAddress из библиотеки kernel32.dll. kernel32.dll — это системная библиотека ядра Windows, которая отвечает за базовые вещи, такие как управление памятью и ввод-вывод. Системные библиотеки — это библиотеки, которые нам дает Windows, они обычно хранятся в папке C:/Windows/System32.

GetProcAddress — это очень важная для нас функция, она даёт нам адрес какой-то функции из определенной библиотеки по её названию. Помните, в начале я писал, что dll-библиотеки называются динамическими, потому что их можно загружать по мере необходимости? Представим, что у нас есть библиотека HelloWorld.dll и в ней есть функция hello_world(). Тогда динамическая загрузка и вызов функции будут выглядеть примерно так:

dll = LoadLibrary(“HelloWorld.dll”); // загружаем библиотеку
hello_world = GetProcAddress(dll, ”hello_world”); // находим функцию по её названию
hello_world(); // вызываем найденную функцию
FreeLibrary(dll); // выгружаем библиотеку из памяти, если она больше не нужна

Примерно такой же код есть и в UnityPlayer.dll. Напомню, что чтобы Unity мог запускать код на C#, ему нужно сначала запустить Common Language Runtime (чем в нашем случае является Mono). UnityPlayer делает что-то вроде этого:

mono_dll = LoadLibrary(“Mono.dll”);
init_mono = GetProcAddress(mono_dll, ”mono_jit_init_version”);
mono = init_mono(…);
// дальше используем mono, чтобы загрузить и запустить код игры

mono_jit_init_version — это функция, которая отвечает за загрузку Mono. Возможно, вы уже догадались, что раз Doorstop вмешивается в запуск игры до того, как Unity запускает свой C#-код, то это должно быть как-то связано с этим Mono. Да, всё так. Делается это в два шага.

Шаг 1. Подделываем GetProcAddress

Когда библиотека Doorstop.dll загружается в память компьютера, она запускает свой код. Этот код находит в памяти уже загруженную библиотеку UnityPlayer.dll, берет её таблицу импорта, находит там GetProcAddress из kernel32.dll и заменяет её на нашу поддельную функцию GetProcAddress из Doorstop.dll. Поддельная функция GetProcAddress смотрит название функции, которую у неё запрашивают. Если это НЕ mono_jit_init_version, то поддельная GetProcAddress просто вызывает настоящую GetProcAddress и дает то, что у неё попросили, тем самым не мешая нормальной работе. Но если у неё запрашивают mono_jit_init_version, то тогда она возвращает поддельную mono_jit_init_version.

Шаг 2. Подделываем mono_jit_init_version

Поддельная mono_jit_init_version сначала вызывает настоящую mono_jit_init_version, чтобы загрузить Mono. Затем она с помощью этого Mono загружает и запускает библиотеку IPA.Injector.dll. Мы уже рассмотрели выше, что IPA.Injector содержит код, который внедряет плагины в Beat Saber. После того, как IPA.Injector завершает свою работу, поддельная mono_jit_init_version отдаёт Mono в Unity.

Unity даже не в состоянии понять, что что-то было не так. Если бы он вызвал настоящую mono_jit_init_version, то он бы получил Mono и начал бы дальше с ним работать. Если Unity запускает поддельную mono_jit_init_version, то он тоже получает Mono — он просто не в курсе, что этим Mono успели воспользоваться для чего-то ещё.

Всё работает именно так, как и написано в лозунге Doorstop: «Run managed code before Unity does». Мы запустили наш управляемый C#-код из IPA.Injector до того, как Unity смог запустить свой C#-код.

Небольшое лирическое отступление. Подмена адресов в таблице импорта — это один из методов хакерских взломов (подробнее тут (на английском): https://pentest.blog/offensive-iat-hooking/). Ещё один интересный факт: в коде UnityDoorstop-BSIPA я нашел благодарность ez (профиль на GitHub) за код, который модифицирует таблицу импорта. Это ещё один пример того, как сообщество совместно работает над решением технических задач и использует код друг друга.

winhttp.dll

Остался один нерешенный вопрос. Во-первых, в описании Doorstop я писал про библиотеку Doorstop.dll. Давайте опять взглянем на файлы, которые IPA.exe устанавливает в игру:

Beat Saber_DataManagedI18N.dll

Beat Saber_DataManagedI18N.West.dll

Beat Saber_DataManagedIPA.Injector.dll

Beat Saber_DataManagedIPA.Injector.pdb

Beat Saber_DataManagedIPA.Loader.dll

Beat Saber_DataManagedIPA.Loader.pdb

Beat Saber_DataManagedIPA.Loader.xml

Beat Saber_DataManagedMicrosoft.CSharp.dll

Beat Saber_DataManagedSystem.Runtime.Serialization.dll

LibsHarmony.1.2.0.1.dll

LibsIonic.Zip.1.9.1.8.dll

LibsMono.Cecil.0.10.4.0.dll

LibsMono.Cecil.Mdb.0.10.4.0.dll

LibsMono.Cecil.Pdb.0.10.4.0.dll

LibsMono.Cecil.Rocks.0.10.4.0.dll

LibsNewtonsoft.Json.12.0.0.0.dll

LibsSemVer.1.2.0.0.dll

winhttp.dll

Как вы можете видеть, Doorstop.dll здесь нет. Во-вторых, даже если бы Doorstop.dll здесь был, то почему Beat Saber или Unity должны его загружать в память? Beat Saber.exe знает про UnityPlayer.dll, поэтому UnityPlayer.dll загружается. UnityPlayer.dll знает про kernel32.dll, поэтому kernel32.dll тоже загружается. Но про Doorstop.dll они ничего не знают и загружать не собираются, как быть?

Решение: прикинуться другой библиотекой, которую загружает Unity и сделать так, чтобы она загрузилась раньше настоящей (и это, кстати, ещё один метод, используемый в хакерских взломах наравне с манипуляциями в таблице импорта, подробнее тут (на английском).

Такой библиотекой в нашем случае является winhttp.dll — это системная библиотека Windows для http-запросов (хранится в C:/Windows/System32). У Unity в какой-то из библиотек в таблице импорта указано, что ей нужна библиотека winhttp.dll, поэтому Windows во время загрузки Unity загружает ещё и её, и только после этого Unity начинает работать.

Doorstop собирается в библиотеку под тем же названием: winhttp.dll. Там содержится код Doorstop, отвечающий за все манипуляции с GetProcAddress и mono_jit_init_version, а ещё там есть таблица экспорта (Export Address Table) со всеми теми же функциями, что в оригинальной winhttp.dll. Загрузка библиотек в Windows устроена так, что Windows сначала проверяет, есть ли нужные библиотеки в папке с программой, а только потом, если ничего не найдено, идет в System32. Поэтому при запуске игры Windows в первую очередь находит наш файл.

Так как у поддельной библиотеки такое же название и такая же таблица экспорта, Windows считает, что это та самая библиотека, которая нужна для игры, поэтому загружает её в память. Поддельная библиотека, в свою очередь, загружает настоящую winhttp динамически (с помощью LoadLibrary) и просто перенаправляет все вызовы из своей таблицы экспорта на адреса настоящих функций (с помощью GetProcAddress). Можно даже размеры сравнить: поддельная библиотека весит 16кб, а настоящая — 960кб.

Вместо winhttp.dll может использоваться любая другая библиотека, используемая Unity, просто разработчики Doorstop выбрали именно её.

В этом месте хочется поблагодарить сообщество BSMG и в особенности DaNike за то, что ответили на мои вопросы в дискорде и помогли разобраться с тем, как работает Doorstop и зачем ему нужна winhttp.dll. Вряд ли они это прочитают, но тем не менее.

Все действия в хронологическом порядке

Если вы уже совсем запутались, то это нормально. Давайте повторим все шаги в хронологическом порядке:

  • запускаем IPA.exe. Все библиотеки копируются в нужные папки игры. Никаких изменений в коде игры пока нет;
  • запускаем игру (Beat Saber.exe);
  • загружается библиотека UnityPlayer.dll, написанная на C++;
  • Windows ищет системные библиотеки, нужные для Unity. Одна из таких библиотек — это winhttp.dll. Windows находит в папке с игрой нашу поддельную библиотеку winhttp.dll и загружает её;
  • в поддельной winhttp.dll вызывается код Doorstop. Он подделывает функцию GetProcAddress из системной библиотеки kernel32.dll;
  • Unity вызывает GetProcAddress, чтобы найти функцию mono_jit_init_version. Так как мы подделали GetProcAddress, она находит поддельную mono_jit_init_version;
  • поддельная mono_jit_init_version загружает Mono;
  • поддельная mono_jit_init_version использует Mono, чтобы загрузить IPA.Injector.dll;
  • IPA.Injector с помощью библиотеки Mono.Cecil модифицирует класс Application из библиотеки UnityEngine.CoreModule.dll так, чтобы он использовал код из IPA.Loader.dll;
  • поддельная mono_jit_init_version передает Mono в Unity;
  • Unity использует Mono, чтобы запустить части движка, написанные на C#;
  • загружается модифицированный класс Application. Вызывается код из IPA.Loader.dll;
  • IPA.Loader загружает моды;
  • Unity использует Mono, чтобы запустить код Beat Saber;
  • оригинальный код игры и код из модов теперь существуют вместе;
  • ?
  • profit.

Конец первой части

На этом часть первая завершена. Мы разобрались, как с помощью манипуляций с библиотеками Windows и Unity, можно добавить моды в любую Unity-игру.

В следующей части мы напишем свой собственный мод для Beat Saber: посмотрим, как моды обмениваются информацией с игрой, как модифицировать поведение оригинальной игры, а также взглянем поближе на Harmony — библиотеку для модификации C#-кода, которая используется моддерами в RimWorld, BATTLETECH, Cities: Skylines, Kerbal Space Program, Oxygen Not Included, Stardew Valley, Subnautica и многих других (по крайней мере так пишет автор Harmony, GitHub).

Подписывайтесь на мой блог

Я начинаю вести свой блог здесь на DTF. Он будет об историях из разработки, о геймдеве и об информационных технологиях в целом. Моя цель — рассказывать людям простыми словами, что технологии — это интересно. Ширяев вон уже этим занимается со своими нейронками, вроде даже успешно. Так что подписывайтесь! Лонги тоже иногда будут.

#статьи

  • 14 июл 2021

  • 0

Игровые модификации: виды и базовый инструментарий для создания модов

А также пять сайтов, где можно отыскать лучшие моды для популярных игр.

Абрикос Абрикосовый для Skillbox Media

Леон Балбери

Считает игры произведениями искусства и старается донести эту идею до широких масс. В свободное время стримит, рисует и часами зависает в фоторежимах.

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

Изменённая модель леди Димитреску из Resident Evil: Village. Скриншот: игра Resident Evil: Village / Nexus Mods

У всех модификаций есть своя специфика, поэтому их принято делить на несколько категорий. Мы проиллюстрируем одну из таких классификаций, взяв за основу исследование, представленное в работе «Моддеры Skyrim: мотивации и модификации» (Modders of Skyrim: Motivations and Modifications). Его авторы — шведские специалисты из Университета Сёдертёрна, Элеонора Хакман и Ульфрик Бьорквист.

Хакман и Бьорквист выделяют восемь основных направлений, связанных с игровыми модами:

  • изменение внешнего вида персонажей (characters);
  • изменение снаряжения (equipment);
  • изменение параметров (item availability);
  • перестановка предметов (placement);
  • улучшение графики (graphics);
  • изменение локаций (locations);
  • погружение в игру (improving immersion);
  • любая помощь в создании модов (custom help).

Нельзя забывать, что моды нередко бывают гибридными. Чем больше в них сочетаний, тем больше шансов, что мод станет масштабным аддоном или самостоятельной игрой.

Существуют и другие классификации. Например, Уолт Скаччи — кандидат наук в области информатики и технологий в Калифорнийском университете — в своём исследовании «Моддинг как open-source-подход к расширению систем в компьютерных играх» (Modding as an Open Source Approach to Extending Computer Game Systems) разделил моды на четыре вида: настройки UI и замена объектов, переделка содержимого игры, машинимы и взлом игр с закрытым исходным кодом.

Правда, для нашего ознакомительного материала такая классификация слишком обширна, и в ней много взаимосвязанных опций (например, взломав игру, можно заменить некоторые детали). Подход Хакман и Бьорквиста проще для понимания, поэтому мы разберём именно его, при необходимости разбив направления на подкатегории с конкретными примерами. Кроме того, последний пункт этой классификации, связанный с помощью в создании модов (custom help), мы заменим на модификации для улучшения интерфейса — изменение UI в контексте нашего материала гораздо важнее.

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

Изменённые модели компаньонов в The Elder Scrolls 5: Skyrim. Скриншоты: игра The Elder Scrolls 5: Skyrim / Nexus Mods

Если в оригинальной игре персонажи не отличаются детализацией и разнообразием, модификации могут исправить этот недостаток. В сборке Total Character Makeover для The Elder Scrolls 5: Skyrim представлены обновлённые ассеты, делающие персонажей более реалистичными. Модели персонажей разных рас наделили дополнительными полигонами и высокодетализированными текстурами. Но всё же оставили прежние черты, чтобы сохранить стиль оригинальной игры.

Согласно данным сайта Nexus Mods, модификацию Total Character Makeover скачали более 1,3 млн раз. А число просмотров страницы с модом достигло почти 6,5 млн.

Созданная моддерами модель Мэрилин Монро в The Sims 4. Скриншот: игра The Sims 4 / The Sims Resource

Игроки любят уникальные аватары. Именно поэтому существует немало модификаций и дополнений, заменяющих оригинальные модели на известных героев из других видеоигр или придающих игровым персонажам внешность реальных людей. Так, на сайтах, посвящённых модификациям для The Sims 4, встречается немало контента со знаменитостями, известными личностями и даже политиками. Ничто не мешает игрокам прожить жизнь в роли Мистера Бина или Мэрилин Монро. Аналогичные замены моделей регулярно встречаются в играх других жанров.

Nude-мод для PC-версии Horizon: Zero Dawn. Скриншот: игра Horizon: Zero Dawn / Nudepatch

К этому подвиду модов можно относиться по-разному, но нельзя отрицать тот факт, что практически на каждую игру найдётся умелец, который разденет героиню (или героя) до нижнего белья, а то и вовсе догола. В основе этих модификаций лежат оригинальные текстуры как минимум лица персонажа. Остальные манипуляции, в том числе доработка модели без одежды, зависят от фантазии и навыков самого моддера. Несмотря на сомнительную ценность таких модов с точки зрения морали и игровых канонов, они остаются актуальными и пользуются спросом среди определённого круга геймеров. Например, мод, «раздевающий» женских персонажей в The Witcher 3: Wild Hunt, скачали более 90 тысяч раз.

Одним из первых Nude-модов в трёхмерных играх был патч Nude Raider, оголяющий модель Лары Крофт. Разработчики Core Design были крайне возмущены распространением такого контента, так как, по их мнению, он дискредитировал персонажа как личность. К тому же этот патч могли установить несовершеннолетние игроки. В 1999 году разработчики потребовали авторов прекратить распространение патча и даже угрожали судебными исками. Вскоре сайт с Nude Raider был закрыт, но от стремлений умельцев раздеть главную героиню это не спасло. В Сети до сих пор можно найти достаточно Nude-модов для любой игры из серии Tomb Raider.

Сюда входит умышленная замена оригинальных персонажей моделями — для создания комической ситуации в игре. Чем сюрреалистичнее изменённая модель, тем больший эффект модификация произведёт. В своё время стал популярным мод для Resident Evil 2 Remake, где вместо Мистера X героев преследует Паровозик Томас — и всё это действо сопровождается музыкой из одноимённого шоу.

Вариант похож на стандартную замену модели и популярен в сообществах игр, сфокусированных на сюжете. Игроки нередко хотят отыграть роль от лица понравившегося второстепенного героя или злодея: подмены персонажей мотивируют игроков вернуться в пройденную игру, чтобы посмотреть на скриптовые сцены под другим углом. Как, например, в моде Model Swapper от camtino, созданном для Cyberpunk 2077. Иногда такой вариант модификаций используется для достижения вышеупомянутого эффекта — превращения игры в театр абсурда. Подобное регулярно практикуется в сообществе Resident Evil в целом и недавней Resident Evil: Village в частности.

Сюда входит создание скинов брони, одежды, оружия персонажа и так далее. Внешний вид экипировки меняют за счёт новых текстур и дополнительных импортированных деталей — или же создают с нуля. Замена снаряжения — одна из самых популярных разновидностей модификаций, как и манипуляции с моделями героев. Обычно внешний вид экипировки меняют, чтобы достичь комического эффекта (как в случае с Resident Evil: Village, где привычное оружие заменили на мухобойку) или же, напротив, усилить погружение в игру.

Вариант брони из мода Immersive Armors для TES 5: Skyrim. Скриншот: игра TES 5: Skyrim / Nexus Mods

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

Например, модификация Better Generators для Fallout 4, созданная expired6978, изменяет свойства оригинальных генераторов внутри игры, позволяя им вырабатывать больше электричества (10 000 единиц мощности вместо 500 единиц). С обновлённой мощностью игрок сократит количество генераторов и сосредоточится на освещении и защите своего поселения, сэкономив при этом на пространстве для будущего строительства.

Генераторы в Fallout 4. Скриншот: игра Fallout 4 / Nexus Mods

В таких модификациях авторы изменяют расположение новых (или существующих) объектов на уровне, чтобы создать более реалистичную картинку. Сюда также относится улучшенная интерактивность окружения. Например, мод Placeable Statics — Move Anything для TES 5: Skyrim — Special Edition даёт игроку больше возможностей во взаимодействии с предметами — чтобы кастомизировать интерьер дома на свой вкус или, наоборот, устроить кавардак.

Беспорядок в доме Бризхоум из TES 5: Skyrim. Скриншот: игра TES 5: Skyrim / Nexus Mods

Комплексная работа по улучшению внешнего вида игры. Достигается за счёт преобразования уже имеющихся в игре 3D-моделей и текстур — или же путём создания новых ассетов. До того как волна официальных ремастеров и ремейков захлестнула рынок видеоигр, были популярны модификации, в которых оригинальный игровой контент переделывали сами игроки. В последние годы для этих целей моддеры всё чаще прибегают к использованию нейросетей и машинному обучению: новые технологии успешно справляются с апскейлом текстур и задников, поэтому время на разработку сокращается.

Так, группа энтузиастов из четырёх человек сделала коллекцию любительских ремастеров трёх первых частей Resident Evil. Благодаря Resident Evil Seamless HD Project игры запускаются на PC с помощью эмулятора Dolphin: классические хорроры могут похвастаться новыми фонами с высокой детализацией, улучшенными текстурами моделей персонажей и даже обновлёнными FMV-роликами.

Пример отрисовки заднего фона с помощью машинного обучения. Скриншот: игра Resident Evil 2 / RE2SHDP

Для апскейла текстур из старых игр также используются нейросети Gigapixel AI (Red Faction, Sonic Adventure 2, Half-Life, Final Fantasy VII), ESRGAN (TES 3:Morrowind, Max Payne, Turok 2), NVIDIA NGX Technology (DOOM) и другие.

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

Одна из самых популярных пользовательских карт в SnowRunner — Red Canyon от Actionjackson@Modio (более 200 тысяч скачиваний). Скриншот: игра SnowRunner / Snowrunner.mod.io

Комплексные модификации, нацеленные на повышение иммерсивности. Помимо всех вариаций, описанных выше, в эту категорию попадают изменения или дополнения, связанные с нарративом: сюжетные линии, уникальные персонажи, новое развитие лора и квесты. Такие моды нередко эволюционируют в самостоятельные игры, которые при должной поддержке даже превосходят оригинал.

В сообществе поклонников S.T.A.L.K.E.R. модификация S.T.A.L.K.E.R. Anomaly считается одним из лучших любительских дополнений. Помимо исправления багов и улучшения визуала, мод добавляет несколько режимов: сюжетный (с дополнительными кампаниями), «песочницу» и «войну фракций». Он также вводит в игру множество NPC, улучшает интерфейс и искусственный интеллект противников, изменяет внешний вид представителей некоторых фракций и выдаёт главному герою трёхмерный КПК. После версии 1.5.1 мод больше не зависит от оригинальных файлов игры и запускается самостоятельно.

Сравнение оригинальной игры и S.T.A.L.K.E.R. Anomaly. Скриншот: S.T.A.L.K.E.R. Anomaly / «ВКонтакте»

Сравнение оригинальной игры и S.T.A.L.K.E.R. Anomaly. Скриншот: S.T.A.L.K.E.R. Anomaly / «ВКонтакте»

Пользовательские модификации интерфейса значительно улучшают игровой опыт. Одни могут быть «косметическими», добавляющими в игру стильные фреймы, иконки или панели. Другие способны сделать весь UI гораздо удобнее, изменив HUD и сделав его более функциональным.

Ещё один вариант UI-модов технически может быть связан с браузером и непосредственно с клиентом игры, если речь идёт об MMO-проектах. Данные, полученные с сервера, улучшают геймплей, но при этом не дают какого-либо преимущества по отношению к другим игрокам. Самый яркий пример таких пользовательских модификаций — аддоны для World of Warcraft. Для этой MMORPG существует почти девять тысяч аддонов с разными задачами — от реализации встроенной навигации по квестам и добавления улучшенной карты до отображения подсказок по тактикам в данжах.

Иногда бездумная установка аддонов создавала кардинально противоположный эффект и становилась объектом шуток и мемов в сообществе WoW. Скриншот: игра World of Warcraft / Reddit

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

Несколько лет назад с такой проблемой столкнулось сообщество World of Tanks. Игра не запрещала устанавливать различные аддоны, и вскоре появились модификации, легальность которых с этической точки зрения была под большим вопросом: анализ брони в бою, отображение цели противника для удачного уклонения, прозрачность объектов на карте и тому подобное. Из-за многочисленных жалоб в 2017 году Wargaming официально запретила подобные аддоны. При этом разработчики World of Tanks не против модов в целом: на сайте игры есть раздел одобренных компанией модификаций.

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

Редактор уровней Valve Hammer Editor из Source SDK. Скриншот: Valve Software

Требования к техническим навыкам зависят в первую очередь от того, какой вид модификации задумал пользователь. В любом случае потребуется знание SDK того движка, на котором написана игра. Речь идёт обо всём необходимом инструментарии для манипуляций с исходным кодом: это и скрипты, и утилиты импорта-экспорта моделей и текстур, и редактор уровней. Но такую творческую свободу игрокам готов предоставить не каждый разработчик. Если Source SDK от Valve с подробнейшей документацией доступен для скачивания всем желающим, то к играм, сделанным, например, на Decima Engine, уже не подступиться без помощи хакеров и написания собственных программ.

К такому виду софта относятся звуковые и графические редакторы, с помощью которых пользователь создаёт контент для «косметических» изменений.

  • Adobe Photoshop — один из самых популярных графических редакторов для создания спецэффектов и текстур для моделей. Для работы с DDS-форматом, в котором сохранены текстуры из большинства популярных игр, потребуются плагины NVIDIA Texture Tools Exporter и Legacy Texture Tools.
  • Adobe Substance Painter — один из инструментов 3D-текстурирования, который позволяет рисовать прямо на 3D-модели и сразу создавать дополнительный маппинг для текстурирования. Полученный результат можно импортировать в другие инструменты для дальнейших манипуляций. Ключевое достоинство Substaince Painter — его совместимость со всеми программами Adobe и операционными системами.
  • Blender — бесплатный редактор для 3D-моделирования и разработки всевозможных проектов, связанных с этой сферой, — от скульптинга и анимаций до рендеринга 3D-артов и текстурирования. Один из самых популярных инструментов среди CG-художников. Активно используется в моддинге из-за возможности импорта и экспорта моделей в различные форматы. Однако из-за широкой функциональности и перегруженного интерфейса новичку придётся потратить немало часов, чтобы изучить все нюансы и добиться желаемого результата.

Стартовое окно редактора Blender
  • Autodesk 3DS Max — ещё один популярный редактор для 3D-моделирования с необходимым набором инструментов и возможностью экспорта. В отличие от Blender, распространяется по подписке.
  • MilkShape 3D — программа для лоу-полимоделирования в старых играх (Half-Life, Unreal Tournament, Quake, The Sims и так далее). Подойдёт тем, кто до сих пор делает моды для ретроигр.

Не исключено, что для создания мода понадобится и звуковой редактор. Их существует довольно много (Sound Forge, Adobe Audition, Audacity). Здесь всё зависит от личных навыков, бюджета и наличия плагинов для экспорта.

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

При этом для моддинга необходимо в первую очередь знание общих концепций кода, а не знание синтаксиса конкретного языка. Код связывает воедино все аспекты игры: уровни и модели, анимацию, искусственный интеллект и так далее. Чтобы не нарушить эту связь при создании масштабного дополнения, моддеру следует понимать все аспекты создания игры.

Знаменитый мод от DPO23, увеличивающий параметры лицевых анимаций персонажей Resident Evil 2: Remake на 500%. Гифка: игра Resident Evil 2: Remake

Знания базовых программ и технических навыков недостаточно для создания качественного пользовательского контента. Софт-скиллы играют не последнюю роль. И начинающий моддер не должен о них забывать.

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

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

Этика. У многих студий есть документы, в которых чётко прописаны правила создания модификаций. Помимо того, что создавать контент с расистским, ксенофобским, сексистским, клеветническим и оскорбительным содержанием просто запрещено, могут также возникнуть проблемы с нарушением авторских прав. Например, сотрудники CD Projekt Red убедительно просят не создавать контент с нарушением прав третьих сторон (авторов книг, комиксов и так далее), реальных людей и брендов. Для разработки такого контента нужно просить разрешение у всех затронутых лиц, в том числе и самих разработчиков.

И, наконец, любовь к игре. Пожалуй, главный фактор, который движет сообществом при создании модов.

Где же можно опубликовать свою модификацию? Как поделиться модом с другими игроками? Ниже — подборка самых популярных ресурсов, где фанаты обычно ищут пользовательские моды для любимых игр.

Мод Immersive Cam для The Witcher 3: Wild Hunt, добавляющий улучшенный вид от третьего лица, похожий на тот, что разработчики демонстрировали на E3 2014. Скриншот: игра The Witcher 3: Wild Hunt / Nexus Mods

Мастерская пользовательского контента, встроенная в клиент Steam. Загрузка модов доступна игроку сразу, без дополнительных манипуляций с установками. Для моддеров существует две опции загрузки контента: открытая и модерируемая. Если в открытой мастерской можно загрузить любой вид модификации, то в модерируемой создаются платные моды, при этом качество контента тщательно отслеживается, а цену устанавливает сам разработчик. При всём удобстве в Steam Workshop есть один минус — модификации есть только для тех игр, разработчики которых сами завели страницу в «Мастерской».

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

По данным сайта, на портале зарегистрировано более 27 млн пользователей и представлено более 310 тысяч файлов для 1323 игр. Общее количество скачиваний за всё время существования достигло более 4,7 млрд.

Крупнейший ресурс модификаций для World of Warcraft, в прошлом известный в игровом сообществе как Curse. В 2016 году сервис выкупила Twitch, а с середины 2020 года права на портал принадлежат компании Overwolf — разработчику сервиса дополнительных приложений для мультиплеерных игр (League of Legends, Hearthstone, CS:GO, Fortnite, PUBG) Все аддоны, загружаемые на CurseForge, проходят строгую модерацию, при этом авторы модификаций зарабатывают на своих творениях путём набора очков, которые можно обменивать на реальные деньги. На сегодняшний день CurseForge поддерживает 13 игр, среди них Minecraft, WoW, TES: Online и Rift.

Один из самых старых порталов, посвящённых модам, основанный ещё в 2002 году. Примечателен тем, что помимо загруженного пользователями контента на сайте собрано немало материалов, посвящённых моддингу и нюансам разработки модификаций. Главная цель портала — объединить разработчиков, игроков, а также их идеи, предоставив им возможность вместе улучшать игры с помощью модов.

Согласно текущей статистике, на Mod DB загружено более 24 тысяч модификаций, которые просмотрели почти 1,5 млрд пользователей.

Австралийский портал для моддинга, созданный в 2018 году основателями Mod DB. Сервис привлекает как авторов пользовательского контента, так и самих разработчиков. Моддерам доступны API и SDK игр, которые есть на сайте, в том числе специальные плагины для игр на Unity, Unreal Engine, GameMaker Studio, Lumberyard и прочих движков. Главное отличие от Steam Workshop в том, что Mod.io — кроссплатформенный сервис, поэтому разработчики могут с его помощью анализировать метрики и статистику по запросам пользовательского контента.

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


Учись бесплатно:
вебинары по программированию, маркетингу и дизайну.

Участвовать

Научитесь: Профессия Разработчик на Unreal Engine с нуля до Middle
Узнать больше

Как создаются моды

monk70

Каким бы шедевральным не получился итоговый продукт с единственно-верным взглядом разработчика, всегда найдутся люди, что захотят его МОДифицировать, изменить или доработать.

Практически все игры могут быть подвергнуты МОДификации, однако лишь исключительная их часть ориентированна на это изначально. Как их создают и интегрируют в игры?

Что есть моды

Подавляющее большинство НЕ фундаментальных частей игр подвергалось моддингу, ибо движок и базовые основы геймплея изменить невозможно.

Не нравится интерфейс? Мод. Не нравятся местные лохмотья? Мод. Не нравятся мыльные текстуры и настройка освещения? Мод!

Сэр!

Сэр!

Моддинг обычной игры

Любая игра в установленном виде, представляет из себя папки с файлами, что на самом деле являются эм… пакетами, хранилищами, архивами отдельных частей игры, например sounds.pak наверняка содержит в себе звуки и музыку.

Если разработчик не планировал поддержку модов игрой, то изобретать приходится народным умельцам, например софт для распаковки популярного формата .pak давно выжидает на специализированных форумах

Мододел вытаскивает из архива одну модельку машины, заменяя её на другую. Подмена файлов является самым распространенным и простым видом моддинга, а в отношении игр, для которых совместимость с модами не планировалась, зачастую единственным…

Если же надоело менять текстурки, звуки и модельки, можно заапгрейдить игру до неузнаваемости, однако здесь это представляет особую сложность, ведь копать придется куда глубже.

Игры для моддинга?

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

Глядя на это становится понятно, почему разработчики не хотят делать атмосферу своих творений жертвами озорников.

Кроме обилия технической документации, исходников и официального софта, в сам лаунчер встроен простейший менеджер модов, а точнее тех же архивов специфичного формата с конкретным содержимым.

Отличительной технической особенностью является возможность создания новых сущностей на уровне движка. Это значит, что можно почитав документацию и кучу форумов, в обычном 3D-редакторе создать модель согласно описанным стандартам, создать ей анимацию, текстуры, а потом записать голос и написать квесты. Это будет совершенно новый NPC, а не замена старому.

Таким образом можно создать даже новую локацию с кучей нового контента и квестов. Такие моды уже есть, а по мнению движка с лаунчером, лучшие из подобных — официальные дополнения Dawnguard и Dragonborn, ведь они видят их тоже как моды наравне с другими, что позволяет например установить официальные дополнения на пиратскую версию и наоборот.

21.09.2017

Желание привнести в игру новые механики, окружение или, говоря проще, изменить что-то на свой лад идет с незапамятных времен. Модифицировать игру куда проще, чем создавать ее с нуля. Уже есть заданные механики, окружение и анимации. Остается только добавлять новый или отрезать уже существующий контент, меняя таким образом изначальный билд. Нередко бывает и так, что сообщество создает специальный софт, который упрощает ряд задач, включая импорт исходников и экспорт новых или измененных файлов в игру. К примеру, еще в 1994 году, благодаря Брендону Уайберу и наличию открытого в DOOM кода, модостроение стало популярно в таких масштабах. Используя DEU (DOOM Editor Utility), написанную Уайбером, игроки получили возможность делать собственные уровни и модификации к игре. На программе “Менеджмент игровых проектов” в Высшей школе бизнес информатики НИУ ВШЭ многие наши слушатели тоже имеют опыт использования программ для создания модов для игр или же интересуются этой темой.

Программы для создания модов - фото 1

Мотивации игроков и мододелов могут быть самыми разными. Начиная от желания разобраться, что находится “под капотом” любимой игры или задаться целью освоить таким образом азы игростроя. Разработка модов может пригодится при устройстве на работу в геймдев, если мододел решает пойти по пути игростроя и стать разработчиком. Кроме того, это помогает увидеть, что представляет собой игра изнутри. Так приходит понимание принципа работы механик, работы скриптов, работы текстур, 2D/3D окружения и объектов, спрайтов, эффектов и т.д. Навыки, которые безусловно пригодятся при дальнейших планах на разработку уже собственного проекта. Точно так же можно прототипировать собственные механики — мод к какой-либо игре вполне может помочь визуализировать задуманное и показать фичу в действии.

По сложности и времязатратам все очень неоднозначно. Начиная от рескина героя, как это часто было в Grand Theft Auto: Vice City и занимало не больше 10-15 минут, заканчивая совсем амбициозными проектами. Иногда модификации могут приобрести чудовищные масштабы и изменить игру до неузнаваемости. Таких примеров не так уж и мало. PlayerUnknown’s Battlegrounds — стала хитом всего за три дня. Брендан Грин под псевдонимом Playerunknown начинал с разработки модов для  ArmA 3, и в дальнейшем создал собственный игровой режим, который сейчас называется «Battle Royale». Режим про H1Z1: King of the Kill. Увидев коммерческий успех этого жанра, Брендан Грин принял решение сделать свою собственную игру в режиме «Battle Royale». Игра была выпущена в рамках программы раннего доступа Steam в марте 2017 года. Разработчики планируют завершить разработку и выпустить готовый продукт до конца 4 квартала 2017 года. Всего за три дня после выхода проект возглавил топ самых популярных игр Steam и за три дня заработал 11 миллионов долларов. А на момент написания статьи продано уже свыше 10 млн. копий игры по цене $29.99.

Та же Dota – изначально была модификацией для Warcraft III. Карта была создана с помощью редактора уровней World Editor для Warcraft III: Reign of Chaos, а с выходом расширения Warcraft III: The Frozen Throne была улучшена.  Сейчас же, Абдул Измаил (IceFrog), один из её создателей, продолжает работать над Dota 2 в Valve. Точно так же было и Counter-Strike. Первая бета Counter-Strike вышла в 1999 году и изначально являлась модификацией Half-Life. Сменив пять версий, игра остается актуальной до сих пор, давно отделившись от Half-Life и перейдя на новый движок Source, а в последствии на Source 2. Огромная работа проделана над S.T.A.L.K.E.R.: Lost Alpha – по сути это огромный аддон, вносящий в игру все, что было вырезано разработчиками из GSC Game World ранее, чтобы воссоздать ранний облик игры из анонсов периода 2003-2004 года. Как и в случае с Dota или с CS, будучи модом к Half-Life 2, Stanley Parable смогла в итоге стать полноценной отдельной игрой и завоевать множество наград.

Под каждую игру, как и в случае с DEU, обычно пишется свой инструментарий, который специально под нее заточен и облегчает работу с дистрибутивом. Но по некоторым играм все же можно выделить самые популярные программы для моддинга.

OpenIV

Программы для создания модов - фото 2
style=»text-align: center;»>

Универсальный редактор и файл-менеджер для PC-версий Grand Theft Auto V, Grand Theft Auto IV/EFLC и Max Payne 3.  OpenIV позволяет редактировать скрытые архивы игры, добавлять и изменять в них файлы нужные файлы, в два клика интегрировать в игру новую текстуру. В режиме редактирования программа автоматически сохраняет все внесенные изменения, но занимаясь созданием модов, лучше вообще всегда не забывать делать бекапы.

Valve Hammer Editor

Программы для создания модов - фото 3

Редактор уровней, разработанный компанией Valve Software для работы с игровыми движками GoldSrc и Source входит в комплект Source SDK — набора программ для создания модификаций к играм, построенным на технологии Source.

Редактор был разработан Беном Моррисом для создания уровней в Quake 1 и размещен в свободном доступе. Начиная с версии 4.0 программа стала работать с Source, на котором написана Half-Life 2, изначально Dota 2 и еще ряд игр от Valve. Редактор поставляется в комплекте Source SDK и доступен к свободной загрузке через сервис Steam для обладателя любой игры, созданной на движке Source. Все обновления программы загружаются с помощью Steam. В Hammer можно создавать уровни для всех современных игр от Valve.

S.T.A.L.K E.R.

Программы для создания модов - фото 4

По S.T.A.L.K E.R.: Зов Припяти и S.T.A.L.K E.R.: Тень Чернобыля вышло уже столько программ, что их объединяют в целые паки.

Сборка для модостроительства ТЧ и ЗП v1.2 объединяет в себе свыше 25 программ и такое количество контента, что игру можно изменить практически до неузнаваемости. В пак входят 165 учебников по модостроению, конвертер для X-ray sdk, набор текстур, 4.XRspawner, Генератор однотипных квестов, DDSPacker v0.1, CharEd v0.2b, статьи по моддингу и еще столько всего, что проще увидеть.

Помимо понимания устройства игры, модификации могут дать небывалый простор для экспериментов и нововведений. Garry’s mod, разработанный Гарри Ньюманом, является физической “песочницей”, где игрок может создавать окружение и проводить любые манипуляции с объектами из игр, созданных на движке Source (CS:S, CS:GO, TF2, HL2 и других игр от Valve). Спустя два года после появления первой версии, 29 ноября 2006 года Garry’s Mod стал платной игрой и до сих пор распространяется через Steam.

В 2012 году Valve сделала огромный шаг навстречу модостроению и запустила Steam Workshop. Сервис позволил всем желающим заниматься созданием собственных модификаций к уже существующим играм, которые его поддерживали. Это дало пользователям возможность не просто делать модификации, но еще и зарабатывать, а также получать фидбек от сообщества прямо в Steam.

Наличие модификаций в портфолио вполне может стать преимуществом при устройстве на работу в геймдев-студию. Умение создать мод может показать, насколько претендент на должность разбирается в левел-дизане, 3D-моделлинге, геймдизайне или каком-либо другом направлении в разработке. Особенно полезно это будет при входе в игровую индустрию, когда выпущенных проектов еще нет, но при этом есть моды, которые ставят себе игроки и пишут хорошие отзывы или которые соответствуют выбранному направлению специальности.

Если вам интересно больше узнать про игровую индустрию, создать и запустить свою игру, или пойти на работу в игровую компанию, то приглашаем на нашу образовательную программу “Менеджмент игровых проектов”. У нас часто проходят и однодневные открытые мероприятия, узнать о которых можно на странице анонсов.


Автор: Михаил Пименов

← Назад к списку

Предположим вы захотели создать мод, но вам мешает недостаток ума информации и вы не можете создать даже папку мода и файл описания.

И я вам поведаю как легко и просто создать свой мод.

Что такое мод. Мод — модификация(ваш кэп), изменяюшая игру как нибудь. Не затрагивает движок, иначе это уже будет отдельной игрой.

Для чего они нужны. Догадайтесь сами.

ПРОГРАММЫ

Текстовый редактор (блокнот не подходит, лучше Notepad++ или тот редактор от самих парадоксов, но он у меня не работает)

Стэлларис

ШАГ 1

Каталог писать лучше на английском или транслитом с нижними подчёркиваниями вместо пробелов.

Что же мы сделали? Мы создали каталог(ваш кэп) мода и его файл загрузки в (имя пользователя)/Документы/Paradox Interective/Stellaris/mod Внизу будет написан путь до файла.

Вот они  

051f08bc0775.png

Hide  

Можем открыть файл с расширением .mod и видим

Войдите или зарегистрируйтесь, чтобы увидеть скрытое содержимое.

Можете подправить необходимые веши.

Вы создали мод! Все изменения проводятся в папке. В случае изменения названия папки надо внести коррективы в .mod

Понравилась статья? Поделить с друзьями:
  • Как пишут моды для minecraft
  • Как пишут мемуары
  • Как пишут медики буквы
  • Как пишут маркеры
  • Как пишут манифест