Unity как создать аддон для своей игры
Текущее время: 29 апр 2024, 11:00
Создания модов для своей игры
Общие вопросы о Unity3D
Сообщений: 2 • Страница 1 из 1
Создания модов для своей игры
Fire Run 21 сен 2014, 23:50
Всем привет! , я новичок в , и особо сильно не разбираюсь что, да как, но в немного шарю, и благодаря этому хоть что то делаю в Unity, ладно, что то я далеко уплыл, так вот как сделать мод для своей игры? , не замена всего текстур пака в игре, а замена допустим текстуры человека или же его модели, или добавить музыку в игру без замены уже присутствующей
Fire Run UNец Сообщения: 10 Зарегистрирован: 26 июн 2013, 15:07
Re: Создания модов для своей игры
beatlecore 22 сен 2014, 00:40
Разработка компьютерной игры в Unity: начните здесь
Любой новый мир начинается с мечты. Unity предлагает все необходимое для ее воплощения. Здесь вы найдете множество советов по разработке вашей первой компьютерной игры. Никаких требований к уровню знаний и навыков: от вас нужно только желание!
Создавайте игру играючи
Начните творить на примере готовых шаблонов Unity Microgame. Каждый из шаблонов имеет свою коллекцию ресурсов Mod, позволяющих играючи изменить исходный шаблон, попутно осваивая основы игрового дизайна, логики взаимодействий, визуализации и многое другое.
LEGO® Microgame
Реализуйте свои творческие идеи с помощью виртуальных блоков LEGOⓇ в нашем новейшем шаблоне Microgame!
FPS Microgame
Взрывайте печеньки, добавляйте симпатичных, но смертоносных роботов, украшайте подземелье. Создайте собственный шутер от первого лица из шаблона FPS Microgame.
2D Platformer Microgame
Разбрасывайте конфетти, устройте феерию света, добавьте бодрости в походку вашего двумерного персонажа в этом милом платформере.
3D Karting Microgame
Набросайте мармеладных мишек, снопы искр и прокачайте свою тачку в веселом картинге.
Your first game jam with Unity
Каждому разработчику нужны единомышленники
Глобальное сообщество Unity предлагает участникам множество способов общения друг с другом. Для новичков доступны гейм-джемы, задачи и группы по интересам (по одной для шаблонов Karting, 2D Platformer и FPS Microgame), которые помогут набраться уверенности и поделиться своими первыми творениями. Мы рады всем желающим!
Made with Unity — Norman’s Island by Little Mountain Animation
Начните творить с Unity
Unity — это самая популярная в мире платформа разработки игр, ведь на ней создано более 50% всех мобильных игр, 60% всего контента для дополненной и виртуальной реальности, а Unity-разработчик — это седьмая по росту популярности профессия согласно недавнему отчету LinkedIn U.S. Emerging Jobs.
Новички могут загрузить Unity бесплатно и начать с готовых ресурсов Unity Microgame и Mod. Учитесь с помощью сотен обучающих материалов, курсов, словарей и игровых наборов — бесплатных или по разумной цене — от Unity и участников нашего потрясающего сообщества.
Вдохновляйтесь, учитесь и творите
Создайте двумерную компьютерную игру
Unity — это ведущая платформа разработки как 2D-, так и 3D-игр. Если вам больше по душе 2D, то здесь можно узнать, как разрабатывать такие игры.
Программирование компьютерной игры в Unity
Вы хотите узнать, как программировать игры? Мы предлагаем множество ресурсов, на примере которых вы сможете научиться программировать на C# в Unity.
Разработайте 3D-игру в Unity
Unity предлагает инструментарий, который поможет вам разработать вашу первую 3D-игру. Начните отсюда, если хотите познакомиться с процессом разработки нового иммерсивного мира для ваших игроков.
Sykoo Sam: начало разработки игр
Sykoo Sam — евангелист Unity в интернете, автор популярного канала, посвященного игровой разработке. Вот несколько советов разработчикам-новичкам.
Thomas Brush: посмотрите это, прежде чем создавать первую игру
Thomas Brush создает игры более 10 лет и готов поделиться мудростью, полезной как начинающим, так и опытным разработчикам.
Как разрабатываются моды для Unity-игр: пишем свой мод
В этой части на примере мода для Beat Saber мы рассмотрим общие принципы разработки модов для Unity-игр, узнаем, какие есть трудности, а также познакомимся с Harmony — библиотекой для модификации кода игр, которая используется в RimWorld, Battletech, Cities: Skylines и многих других играх.
Хоть эта статья и похожа на туториал, как написать свой мод для Beat Saber, ее цель — показать, какие принципы используются при создании любых пользовательских модов и какие проблемы приходится решать при разработке. Все, что здесь описано, с некоторыми оговорками применимо для всех Unity-игр как минимум в Windows.
В предыдущей серии
Информация из первой части не нужна для понимания того, что будет происходить здесь, но все равно советую с ней ознакомиться.
Вот ее краткое (очень) содержание:
Программные моды (также известные как плагины) — это 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 указывает на файл с описанием схемы для валидации формата. Файл лежит на 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.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.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: :"; _nextTextUpdate += TEXT_UPDATE_PERIOD; > >
Теперь обновляем наш класс Plugin, чтобы он создавал объект TimeTracker:
[OnStart] public void OnStart() < logger.Debug("OnStart"); GameObject timeTrackerGo = new GameObject("TimeTracker"); timeTrackerGo.AddComponent(); 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: :"; _activeTimeText.text = $"Active: :"; _nextTextUpdate += TEXT_UPDATE_PERIOD; > >
Теперь добавляем метод для включения и выключения отслеживания активного времени:
private void SetTrackingMode(bool 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()
Собираем проект, копируем 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();
Этому методу все равно, на какой сцене компонент, что за объект и активен ли он. Если компонент создан, он будет найден. Но нужно как-то найти момент времени, когда этот компонент создан. Можно предположить, что он висит на каком-то объекте на одной из сцен в геймплее. Мы уже выяснили, что геймплейные сцены каждый раз создаются заново. У нас есть обработчики событий OnSceneLoaded и OnActiveSceneChanged, поэтому мы можем отловить там сцену GameCore и в этот момент попробовать получить ссылку на GamePause. Проблема в том, что он может создаваться динамически чуть позже, чем загружаются сцены, поэтому тут есть два варианта: поискать в игре событие, которое срабатывает после того, как GamePause создан (вряд ли такое есть), либо вызывать Resources.FindObjectsOfTypeAll каждый кадр, пока не найдем компонент. Например, через корутину:
IEnumerator InitGamePauseCallbacks() < while (true) < GamePause[] comps = Resources.FindObjectsOfTypeAll(); 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
Нам нужно всего одно значение, чтобы отслеживать состояние паузы, поэтому структура избыточна, но как я уже писал выше, я люблю ясный код. 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-редактора.
А потом разработчики выпускают новую версию игры, в которой они поменяли логику, которая использовалась в моде, и нужно делать все сначала.
- unity
- реверс-инжиниринг
- разработка игр
Unity Unity, моддинг
Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.
Примечание: Ваш пост требует одобрения модератора, прежде чем станет видимым.
Подписчики 0
Последние посетители 0 пользователей онлайн
Ни одного зарегистрированного пользователя не просматривает данную страницу
Похожий контент
Добавление моделей в моды
Этот гайд поможет Вам наиболее простым и понятным способом добавить любую понравившуюся модель оружия\брони\коня и др. из одного мода в другой.
Рассматриваемый здесь процесс предназначен для использования только в ЛИЧНЫХ целях в конкретно в Вашем играемом моде.
Для работы понадобятся две несложные в освоении программы :
OpenBRF (v0.0.81b) :
Скрытое содержимое
Для просмотра скрытого содержимого необходима РЕГИСТРАЦИЯ на сайте.
Morgh’s Mount & Blade WB/WFAS Editor V-1.50 : Скрытое содержимое
Для просмотра скрытого содержимого необходима РЕГИСТРАЦИЯ на сайте.
Рассмотрим на примере Латного доспеха Сокола из замечательного мода Prophesy of Pendor v3.9.5 (от 13.03.2020) :
Скрытое содержимое
Для просмотра скрытого содержимого необходима РЕГИСТРАЦИЯ на сайте.
Русификатор (Авторы : Dalion и firon, на основе работ Faraon67 и Валуа) уже официально включен в мод.
Всем, кто еще не играл, советую обязательно ознакомиться.
А тем, кто захочет открыть по нему на форуме тему, буду благодарен.
1) Если мод, из которого Вам нужно взять модель, русифицирован, то ищем название нужной модели в русификаторе :
Блокнотом (или другим текстовым редактором) открываем файл Prophesy of Pendor V3.9.5\languages\ru\item_kinds.csv и через Поиск находим строку с названием брони Латный доспех Сокола :
itm_falcon_plate|Латный доспех Сокола
2) Блокнотом открываем файл Prophesy of Pendor V3.9.5\item_kinds1.txt
В этом файле прописываются все предметы и их характеристики используемые в моде.
В поиск вставляем itm_falcon_plate
и видим строку :
itm_falcon_plate Falcon_Plate Falcon_Plate 1 falcon_plate 0 16842765 0 10033 973078948 23.000000 10 2 56 20 18 0 0 0 0 0 0 0
, нас пока в ней интересует только название нужной модели в доспехе. Это — falcon_plate
Имя любой модели будет всегда находится только на этом месте.
Если Вы играете в не русифицированную версию мода, то сразу начинайте со 2 пункта, вводя в поиск английскую версию нужной Вам брони.
3) Вот теперь нам и необходима программа OpenBRF. С ее помощью мы можем просмотреть все модели используемые в моде.
Они хранятся в папке Resource в специальных архивах с расширением .brf
Открываем первый же архив в списке, например — ACOK_carpets.brf
Воспользуемся копипастой с клавиатуры Ctrl+F
В открывшемся окне вводим название нашей модели falcon_plate
Из выпавшего списка выбираем нужный Mesh (берите без lod’ов)
Таким образом мы находим архив с нужной моделью — pop_women_armor.brf
Данный метод (через Ctrl+F) хорош тем, что сразу показывает все модели, прописанные в файле item_kinds1.txt
Они выделяются жирным синим цветом. Мусорные модели, которые автор поленился удалить при разработке мода, будут намного бледнее.
В нашем случае опытные авторы мода сделали грамотный архив.
4) Теперь этой же программой нам надо создать в моде, в который мы хотим добавить этот доспех, новый архивный файл .brf
Открываем в OpenBRF папку Resource в этом моде (для примера я выбрал стандартный модуль Native), не закрывая предыдущего окна с найденной моделью.
Очень удобно, что OpenBRF может запускаться одновременно во многих отдельных окнах.
В левом верхнем углу File — New
В предыдущем окне с моделью falcon_plate через клавиатуру нажимаем Ctrl+C , а в новом окне Ctrl+V . Все, модель перенесена.
Далее советую обязательно сохранить новый архив :
File — Save As. — Вводим имя (напр. ALISA)
ВНИМАНИЕ ! 1) При сохранении правильно выберите тип файла (В нашем случае WarBand Resource)
2) Должна в названии быть только латиница.
3) Сверху в окне сохранения укажите нужный путь в папку Native\Resource
5) Необходимо обязательно повторить копипаст с клавиатуры Ctrl+C, Ctrl+V для материала и текстур этой модели.
Для этого нажимаем активную надпись синего цвета Material , и плавно переходим на окно с материалами.
В этом окне последовательно переходим на все используемые текстуры и тоже копируем их в новый архив.
Возврат к предыдущему окну через синюю надпись (back).
Переместить строку внутри окна можно через Alt — стрелки вверх\вниз.
Просмотреть несколько строк можно выделив их ЛКМ.
Сохраняем вновь наш созданный архив в нужную папку с модом.
ВНИМАНИЕ ! Обязательно проверьте место расположения шейдеров в архиве, откуда берем модель.
Для этого в окне материала нажимаем на синюю надпись Shader.
Если Вас перебросило на архив из самой игры, то лишних телодвижений уже делать не надо.
Путь CommonRes\core_shaders.brf увидите вверху окна.
Если Вас приведет на другой архив в самом моде (в некоторых модах используются свои шейдеры), то надо будет тупо скопировать этот указанный архив с шейдерами
в папку Resource Вашего мода, и не забыть прописать его в файле module.ini
Для всех lod’ов данной модели брони повторяем операции из 4 и 5 пункта.
Лоды — это упрощенные варианты моделей, используемые при дистанцировании модели в игре от камеры. Для оптимизации игры и уменьшения нагузки на железо.
6) Находим в папке Prophesy of Pendor V3.9.5\Textures нужные текстуры и копируем их в аналогичную папку Вашего мода.
Имена нужных текстр узнаем в Вашем созданном архиве.
В нашнм случае их три (с нормалью и спекуляром) :
armour_order_falcon.dds
armour_order_n.dds
armour_order_s.dds
7) Теперь, для того чтобы наш мод увидел новую модель, необходимо созданный архив прописать в файле module.ini
Открываем его блокнотом и в самом низу после последних строк начинающихся на load_resource = или load_mod_resource =
прописываем свою строку. В нашем случае это :
8 ) Далее необходимо прописать в файле Вашего мода item_kinds1.txt новый созданный доспех.
Можно в самый низ этого файла (обычно перед нижней строкой )
itm_ccoop_new_items_end Items_End Items_End 1 shield_round_a 0 0 0 1 0 0.000000 100 0 0 0 0 0 0 0 0 0 0 0
просто добавить блок строк из Prophesy of Pendor V3.9.5 :
itm_falcon_plate Falcon_Plate Falcon_Plate 1 falcon_plate 0 16842765 0 10033 973078948 23.000000 10 2 56 20 18 0 0 0 0 0 0 0
1
16
0
, сохранив пробелы между строк и нужные отступы.
ВНИМАНИЕ ! Не забудьте во второй строке item_kinds1.txt изменить прописанное у Вас в моде трех или четырехзначное число на количество добавляемых новых предметов.
Иначе игра их просто не увидит.
А можно самому задать любые необходимые характеристики для Вашего нового доспеха.
9) Здесь нам очень поможет программа Morgh’s Mount & Blade WB/WFAS Editor V-1.50
Я ее инсталлирую просто на Рабочий стол, а когда мне надо поработать с ней в каком то моде, тупо копирую в его папку.
Тогда она сама найдет нужный путь к файлам. Потом эту прогу можно будет из папки с модом безболезненно удалить.
Открываем программу и соглашаемся на сохранение конфигурации.
Эта очень полезная программа работает с пятью текстовыми файлами :
troops.txt — здесь можно изменять всех героев и юнитов.
factions.txt — здесь можно изменять фракции.
parties.txt — города\замки\деревни.
party_template.txt — отряды и гарнизоны.
item_kinds1.txt — уже знакомый нам файл со всеми предметами в моде. Вот он и будет нужен в данном случае.
Находим через поиск нужную строку (у нас это itm_falcon_plate) и правим характеристики доспеха под себя любимых.
Описание всех окон я даю по тексту со Всадников Кальрадии с моими небольшими добавлениями :
Знаком . я выделил парамеры, на которые особенно обращаю внимание при создании своих мини-модов.
Не пугайтесь обилию текста в этом гайде. Сделав все по нему пару раз, дальше алгоритм запоминается.
Все неясности прошу обсуждать в данной теме.
По прогрмме OpenBRF я возможно в дальнейшем оформлю отдельный гайд, т.к. интересных возможностей у нее еще очень много.
Хитрости, особенности, фичи и баги
Предлагаю здесь насыпать вышеобозначенных «особенностей» сей игры. Я насыплю насчет торговли относительно версий 1.0.0-1.0.11:
— прокачка скила торговли происходит только если вы купили товар в одном месте дешево и продали в другом месте дорого.
— разъясню предыдущий пункт- если товар имеет происхождение военной добычи или рейда деревни- с него не будет прокачки навыка торговли
— предельно упрощенно схема прокачки навыка выглядит так- купил в одном городе 100 рыбы по 7 и продал в другом городе по 18. все. гешефт и прокачка скила.
— чтобы понять, сделка качнула навык или нет- до сделки смотрим опыт в навыке и запоминаем и смотрим после сделки. если капнуло- то все ок, если нет- то надо дочитать этот «гайд»
— есть товары, которые не дают опыта при барыгании помимо трофейных- это кузнечные ресы, притом есть ОЧЕНЬ существенная особенность- к этим ресурсам относиться и древесина. объясню коварство торговли древесиной в двух иллюстрациях: а) разобрал кучу вил и получил 100 дерева, которое выгодно толкнул -> прокачки шиш ибо рес не был куплен б) имея 100 дерева, разобрал оружий на еще 100 дерева и выгодно толкнул 100 дерева -> прокачки шиш ибо сначала продалось накрафченое дерево, которое не было куплено.
— торговля в деревнями навык качает, но как правило там и товара очень мало и бегать можно офигеть. если уж есть желание, то дам подсказку- у деревни есть профильный товар, который можно увидеть наведя на деревню мышку.
— торговать с караванами теоретически можно, но следует учитывать то, что как правило у них не корректно отображаються цены и изменения цен. т.е. за сделку деньги выдадут честно, но как правило прокачки торговли не будет.
— а теперь самая главная особенность торговли- чем больше товара вы покупаете, тем больше становиться его цена и соответственно чем больше вы продаете товара, тем он дешевле. для иллюстрации: я имею 200 рыбы, которую купил по 10, с разгона показываеться продажная цена 20, далее можно сделать глупость и перетащить весь товар и недоглядев его продать за 6, а можно продать только 40-50 рыбок по 16-17 и получить гешефт и прокачку навыка.
— идея «круто забарыгать» харчами может кончиться тем, что войска в процессе передвижения просто сожрут «вскусняшки», так что имхо пытаться забарыгать харчем кроме зерна и рыбы кончиться потерей бабла.
— у торговли есть еще одна огромная трудность — после загрузки игры полностью отваливаются все данные о том, что и почем было куплено и, соответственно, никакого расчета гешефта не происходит (но таки прибыль есть).
кстати, касательно караванов- у них бабло обновляется каждую сессию торговли и можно одному и тому же каравану слить товару на несколько его бюджетов.
данный текст написан для гс и размещать его в других местах нельзя, ибо у злодея появиться гонорея и сифилис, а также его будут ректально любить бегемоты. впрочем, если много раз облыбзать мои тапки, то мое величество может снизойти.