Как Разрабатываются Моды Для Игр На Unity: Пишем Свой Мод

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

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

Все описанное здесь, с некоторыми оговорками, применимо ко всем играм Unity, по крайней мере, на Windows.

Как разрабатываются моды для игр на Unity: пишем свой мод

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



В предыдущем эпизоде

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

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

Если в игре нет встроенной поддержки модов, то никаких dll-файлов она запускать не будет. Поэтому для реализации сторонних модов используются специальные библиотеки, например БепИнЭкс или МПА .

Используется в Beat Sabre БСИПА - улучшенная версия IPA. Сначала его просто адаптировали специально для Beat Sabre, но теперь он технически значительно превосходит оригинальный IPA и может использоваться для любых Unity-игр.



О Beat Sabre и моде, который мы сделаем

Beat Sabre — одна из самых популярных игр для VR-гарнитур.

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

Покажет текущее время (обычные часы), количество минут, проведенных в игре с момента ее запуска, и количество минут, проведенных в игре активно, т.е.

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

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

Я разбил все на 5 шагов; в конце каждого шага будет краткий вывод об особенностях разработки мода.

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

Для полного понимания желательно знать основы Unity: работа со сценами, иерархия объектов, компоненты и их жизненный цикл.



Подготовка

Во-первых, нам нужно сделать игру подходящей для модов.

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

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

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



Примечание о версиях

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



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

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

Первые шаги хорошо описаны на сайте.

Группа моддинга Beat Sabre (далее просто БСМГ).

К сожалению, там описаны только начальные шаги.

Для создания проекта доступно несколько шаблонов Visual Studio — просто возьмите тот, который вам нравится, и создайте проект на основе шаблона.

В этой статье мы пойдем по более сложному пути и создадим проект с нуля.

Берем нашу любимую среду разработки для C# (я использую Rider), создаем новый проект C#, в качестве целевой сборки выбираем Class Library и выбираем версию .

NET, совместимую с Unity (я использую 4.7.2).

Получаем пустой проект. Теперь давайте создадим файлы мода.



манифест.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. Это не должно нас слишком беспокоить, мы просто добавляем это и забываем.

В dependOn указываем какие сторонние моды используем в своем моде.

BSIPA использует эту информацию для определения порядка загрузки файлов DLL. gameVersion и версия используются семантическое управление версиями .



Плагин.

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. Главное, чтобы пространство имен совпадало с именем, которое мы указали в манифесте — в данном случае это BeatSaberTimeTracker. На этом этапе мы просто будем писать в лог, если был вызван какой-то метод. Чтобы это работало, вам нужно сообщить компилятору, где определены атрибуты [Plugin], [Init], [OnStart] и [OnExit].

Для этого добавьте файл IPA.Loader.dll в зависимости в свойствах проекта.

Будем считать, что моды уже внедрены в игру, а это значит, что все необходимые библиотеки уже находятся в папке Beat Sabre где-то в папках Steam. Библиотеки игры, Unity, системные библиотеки и файлы IPA расположены в папке Beat Sabre/Beat Sabre_Data/Managed. Все просто добавляют файлы прямо из папки Steam в проект и заливают на GitHub, тут нечего стесняться.

сами БСМГ они советуют тебе это сделать .

Собираем наш мод, копируем полученный dll-файл в папку Beat Saber/Plugins и запускаем игру.

Для простой отладки не обязательно подключать VR-шлем; вы можете запустить игру из терминала с флагом fpfc. Игра запустится в режиме отладки с управлением мышью.

Этого достаточно, чтобы потыкать кнопки в главном меню.

После этого выйдите из игры, зайдите в папку Beat Sabre/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 создаст холст в мировом пространстве, добавит к нему два текстовых поля и будет обновлять значения в них раз в секунду.

Создание объектов в 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. В каждом поле я отобразил его координаты.

Это помогло мне начать как-то ориентироваться в координатах и масштабе сцены.



Как разрабатываются моды для игр на Unity: пишем свой мод

В 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; } }

Теперь мы обновляем наш класс плагина, чтобы он создавал объект 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 для Холст. Вы можете получить их все там же, в папке с игрой.

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



Как разрабатываются моды для игр на Unity: пишем свой мод

Давайте посмотрим логи:

[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()

Все отлично, наш мод работает и уже полезен.

Пока он живет своей жизнью — на игру он не влияет, а игра не влияет на него.

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

Мы займемся этим дальше.



Как разрабатываются моды для игр на Unity: пишем свой мод



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

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

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

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

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

На этом этапе мы начинаем контактировать с игрой.

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

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

Обновляем метод 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 Sabre/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 в Плагины, запускаем игру, проверяем.



Как разрабатываются моды для игр на Unity: пишем свой мод



Как разрабатываются моды для игр на Unity: пишем свой мод



Как разрабатываются моды для игр на Unity: пишем свой мод

Если бы мы только разрабатывали мод для Beat Sabre, то могли бы остановиться на этом этапе.

Мод готов, он делает то, что мы хотели и так, как мы хотели.

Он использует сторонний мод BS_Utils, но его используют почти все моды.

BS_Utils поддерживается одним из основных разработчиков сообщества BSMG, поэтому вам не придется беспокоиться о том, что в какой-то момент он перестанет работать.

Но это познавательная статья, поэтому пойдем дальше.

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



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

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

Например, в Beat Sabre мод 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 Sabre использует разные сцены для разных компонентов и загружает их в аддитивном режиме.

Интерфейс находится на одной сцене, платформа с плеером — на другой.

Анализируем логи и делаем вывод: отследить переход к основному игровому процессу можно, например, при активации сцены 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; } }



песняPaused и песняUnpaused

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

Теперь нам нужна библиотека, содержащая код Beat Sabre. В папке «Beat Sabre/Beat Sabre_Data/Managed» находятся 2 библиотеки: Main.dll и MainAssembly.dll. Сначала я покопался в MainAssembly.dll, в результате чего потратил два дня на отладку очень странного поведения.

Оказалось, что и Main.dll, и MainAssembly.dll по какой-то причине содержат определения одних и тех же классов.

Я использовал MainAssembly.dll, а игра использовала классы из Main.dll. Возможно, произошла какая-то ошибка при сборке сборки у разработчиков игры.

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

Веб-сайт BSMG советует использовать dnSpy .

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

Но, судя по описанию, штука полезная — это не только декомпилятор, но и отладчик, умеющий подключаться к Unity-процессам.

Дальше идет рутина: берем содержимое Main.dll и ищем класс, который делает то, что нам нужно.

Это сложно, но другого выхода нет. Если только вы не сможете зайти на канал BSMG Discord и спросить.

Вам скорее всего ответят, потому что там много людей, которые уже декомпилировали Main.dll и что-то там искали (и нашли).

Рано или поздно мы найдем класс GamePause, отвечающий за включение и выключение паузы в игре.

Он имеет два метода: пауза и возобновление.

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

На этом этапе начинается волшебство, мы взглянем на Гармония — библиотека для модификации кода C#, которая используется моддерами во многих играх.

Его автор – Андреас Пардейке ( Веб-сайт , GitHub ), работает ведущим разработчиком/архитектором iOS в шведской полиции ( Шведское полицейское управление ).

В отличие от библиотеки Mono.Cecil из предыдущей статьи о модах, которая изменяет и перезаписывает dll-файлы с помощью .

NET-сборок, Harmony модифицирует код во время выполнения программы (рантайма).

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

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

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

Существует несколько типов патчей:

  • Префикс .

    Патч, который вызывается перед выполнением метода.

    С его помощью вы можете перехватить и изменить аргументы метода или решить, вызывать ли сам метод или немедленно выйти.

  • Постфикс .

    Патч, который вызывается после выполнения метода.

    Вы можете перехватить и изменить возвращаемое значение.

Теги: #Разработка игр #C++ #unity #обратное проектирование
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.