В этой части на примере мода для Beat Sabre мы рассмотрим общие принципы разработки модов для игр Unity, выясним, какие возникают сложности, а также познакомимся с Harmony — библиотекой для модификации игрового кода, которая используется в RimWorld, Battletech, Cities: Skylines и многих других играх.
Хотя эта статья похожа на учебник о том, как написать собственный мод для Beat Sabre, ее цель — показать, какие принципы используются при создании любых пользовательских модов и какие проблемы приходится решать при разработке.
Все описанное здесь, с некоторыми оговорками, применимо ко всем играм Unity, по крайней мере, на Windows.
Источники изображений: 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 указывает на файл, описывающий схему для проверки формата.{ "$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": {} }
Файл находится на 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. В каждом поле я отобразил его координаты.
Это помогло мне начать как-то ориентироваться в координатах и масштабе сцены.
В 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-файл, копируем в папку плагинов, запускаем игру и любуемся результатом.
Давайте посмотрим логи: [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: взаимодействуйте с логикой самой игры
На этом этапе мы начинаем контактировать с игрой.Мы будем считать активное время, проведенное в игровом процессе, и скрывать мод пользовательского интерфейса, когда он не нужен.
Для этого вам необходимо научиться определять переходы из меню в основной игровой процесс и определять, поставлена ли игра на паузу.
Обновляем метод 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 в Плагины, запускаем игру, проверяем.
Если бы мы только разрабатывали мод для 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 в терминах самой библиотеки называются патчами ( патчи ).
Существует несколько типов патчей:
- Префикс .
Патч, который вызывается перед выполнением метода.
С его помощью вы можете перехватить и изменить аргументы метода или решить, вызывать ли сам метод или немедленно выйти.
- Постфикс .
Патч, который вызывается после выполнения метода.
Вы можете перехватить и изменить возвращаемое значение.
-
Gmail.ru Зарегистрирован Как Торговая Марка
19 Oct, 24 -
Sali — Ваш Язык Программирования.
19 Oct, 24