В первой части статьи нам удалось загрузить и заставить работать аддон, который был создан отдельно от основного проекта Unity.
В текущем варианте скрипты можно разрабатывать автономно. Для этого не нужен ни исходный код проекта, ни сам Unity. Для разработки аддона нужно только VisualStudio, файл API.dll и откомпилированная версия самой игры.
Это здорово, но нам бы хотелось большего. Дело в том, что разработка скриптов - сложно и долгое занятие, требующая много отладки, настройки и тестирования. А в текущей реализации плагин загружается в игру только при старте. Поэтому что бы запустить плагин второй раз, нужно выключить игру, откомпилировать плагин и снова запустить игру. Это долго и неудобно.
Нам хотелось бы что бы аддон можно было обновлять когда игра запущена. И что бы измененный плагин сразу подхватывался игрой и начинал выполняться без перезагрузки игры.
Отслеживание изменений плагина в рантайме
Для отслеживания факта изменения плагина можно использовать класс FileSystemWatcher. После того, как файл плагина найден - создаем FileSystemWatcher и начинаем отслеживать его изменения. Как только они возникли - будем загружать аддон снова.
Класс PluginsController будет выглядеть следующим образом:
usingSystem;usingUnityEngine;usingScripts;usingSystem.Linq;usingSystem.IO;usingSystem.Reflection;publicclass PluginsController : MonoBehaviour
{
ScriptingBase script;
FileSystemWatcher watcher;privatevoid Start(){//подсовываем свою длл вместо любой другой
AppDomain.CurrentDomain.AssemblyResolve+=(o, e)=> Assembly.GetExecutingAssembly();//ищем файлы *Addon.dll вокруг себяvar addonFile = Directory.GetFiles(Path.Combine(Application.dataPath, ".."), "*Addon.dll", SearchOption.AllDirectories).FirstOrDefault();//если файл аддона найденif(addonFile !=null){//запускаем отслеживание изменений файла плагина
watcher =new FileSystemWatcher(Path.GetDirectoryName(addonFile), Path.GetFileName(addonFile));
watcher.NotifyFilter= NotifyFilters.LastWrite;//будем загружать аддон снова, если он был изменен
watcher.Changed+=(o,e)=> LoadAddon(addonFile);//загружаем аддон
LoadAddon(addonFile);}}void LoadAddon(string addonFile){//загружаем дллvar ass = Assembly.LoadFile(addonFile);//ищем все классы, унаследованные от ScriptingBaseforeach(var type in ass.GetTypes()){//этот тип унаследован от ScriptingBase?if(typeof(ScriptingBase).IsAssignableFrom(type))// не абстрактный?if(!type.IsAbstract){//создаем экземпляр скрипта
script = Activator.CreateInstance(type)as ScriptingBase;break;}}}privatevoid Update(){//обновляем скриптif(script !=null)
script.Update();}}
Оно БЫ работало, если бы не много всяких НО.
Что бы не тянуть кота за хвост, опишу сразу все проблемы, которые возникнут.
Во-первых, после того, как длл загружена в игру, вы уже не сможете ее изменить, потому что она будет заблокирована до тех пор, пока игра не закроется. Так работают все dll и exe файлы. Ничего не поделаешь.
Во-вторых, даже если вы сможете изменить длл, то она не загрузится повторно в игру. И дело вот в чем. У ассембли есть внутреннее имя. Если фреймворк загружает длл, он смотрит на это имя и если оно совпадает с именем длл, которая уже загружена, то второй раз она загружаться не будет. И в то же время выгрузить старую длл из памяти - нельзя, фреймворк не позволяет этого сделать.
Тем не менее, обе проблемы решаемы.
Решение проблемы с изменением dll
Поскольку загруженная длл не может быть изменена, сделаем по-другому. Когда файл плагина найден - просто скопируем его во временную папку и уже оттуда будем его загружать.
Тогда исходный файл плагина не будет заблокирован, и его можно будет менять.
Решение проблемы с невозможностью повторной загрузки dll
А вот здесь немного посложнее. Когда вы компилируете свой аддон, то имя проекта записывается внутрь длл. Даже если вы переименуете сам файл, то внутренне имя все равно останется прежним.
Возможный выход - менять каждый раз имя проекта в VS. Но это очень неудобно.
Поэтому сделаем по-другому. Если посмотреть внутрь длл, то можно увидеть, что имя ассембли лежит в файле, в незашифрованном виде, ограниченное с двух сторон нулевыми байтами:
Если найти эту последовательность внутри файла и заменить на случайные буквы, то имя ассембли поменяется!
Таким образом нам нужно сделать следующее:
1) Открыть файл ассембли, найти строку, совпадающую с именем файла и заменить это строку на случайную последовательность символов.
2) Сохранить измененную длл во временную папку под случайным именем.
3) Подгрузить измененную длл в движок игры.
Для удобства, сделаем отдельный класс DllHelper, который будет заниматься манипуляциями с длл:
usingSystem.IO;usingSystem.Text;class DllHelper
{staticSystem.Random rnd =newSystem.Random();// Изменение внутреннего имени ассембли и копирование ее во временный файлpublicstaticstring CopyAndChangeStrongNameOfDll(string file){//читаем длл как массив байтvar bytes = File.ReadAllBytes(file);//ищем имя ассембли внутриvar name ='\x0'+ Path.GetFileNameWithoutExtension(file)+'\x0';var pattern = Encoding.UTF8.GetBytes(name);var index = FindSequence(bytes, pattern);//если имя найдено - заменяем его на случайную последовательность буквif(index >=0)for(int i =1; i < pattern.Length-1; i++){var letter = rnd.Next('a', 'z');
bytes[i + index]=(byte)letter;}//сохраняем измененную длл во временный файлvar temp = Path.GetTempFileName();
File.WriteAllBytes(temp, bytes);//возвращаем имя временного файлаreturn temp;}staticint FindSequence(byte[] self, byte[] candidate){for(int i =0; i < self.Length; i++)if(IsMatch(self, i, candidate))return i;return-1;}staticbool IsMatch(byte[] array, int position, byte[] candidate){if(candidate.Length>(array.Length- position))returnfalse;for(int i =0; i < candidate.Length; i++)if(array[position + i]!= candidate[i])returnfalse;returntrue;}}
А в классе PluginsController будем вызывать метод CopyAndChangeStrongNameOfDll перед тем как открыть файл плагина:
C#
1
2
3
4
5
6
7
//меняем внутренне имя и копируем длл во временный файлvar temp = DllHelper.CopyAndChangeStrongNameOfDll(file);//загружаем дллvar ass = Assembly.LoadFile(temp);//...
usingSystem;usingUnityEngine;usingScripts;usingSystem.Linq;usingSystem.IO;usingSystem.Reflection;publicclass PluginsController : MonoBehaviour
{
ScriptingBase script;
FileSystemWatcher watcher;bool needToReloadAddon;string addonFile;privatevoid Start(){//подсовываем свою длл вместо любой другой
AppDomain.CurrentDomain.AssemblyResolve+=(o, e)=> Assembly.GetExecutingAssembly();//ищем файлы *Addon.dll вокруг себя
addonFile = Directory.GetFiles(Path.Combine(Application.dataPath, ".."), "*Addon.dll", SearchOption.AllDirectories).FirstOrDefault();//если файл аддона найденif(addonFile !=null){//запускаем отслеживание изменений файла плагина
watcher =new FileSystemWatcher(Path.GetDirectoryName(addonFile), Path.GetFileName(addonFile));
watcher.NotifyFilter= NotifyFilters.LastWrite;
watcher.EnableRaisingEvents=true;//будем загружать аддон снова, если он был изменен
watcher.Changed+=(o, e)=> needToReloadAddon =true;//загружаем аддон
LoadAddon(addonFile);}}void LoadAddon(string file){//меняем внутренне имя и копируем длл во временный файлvar temp = DllHelper.CopyAndChangeStrongNameOfDll(file);//загружаем дллvar ass = Assembly.LoadFile(temp);//ищем все классы, унаследованные от ScriptingBaseforeach(var type in ass.GetTypes()){//этот тип унаследован от ScriptingBase?if(typeof(ScriptingBase).IsAssignableFrom(type))// не абстрактный?if(!type.IsAbstract){//создаем экземпляр скрипта
script = Activator.CreateInstance(type)as ScriptingBase;break;}}}privatevoid Update(){//если нужно перезагрузить аддон - перезагружаемif(needToReloadAddon){
LoadAddon(addonFile);
needToReloadAddon =false;}//обновляем скриптif(script !=null)
script.Update();}}
Запускаем все вместе, и оно работает!
Теперь код аддона можно менять, компилировать и результат работы скриптов сразу будет отображаться в игре.
Система плагинов как для Wf так и для Unity это просто космос.
Было бы круто увидеть перекомпиляцию приложения "на лету" и побольше бы подобных статеек
Всем привет.
По скидке в "черную пятницу" взял себе новый ноутбук Lenovo ThinkBook 16 G7 на Амазоне:
Ryzen 5 7533HS
64 Gb DDR5
1Tb NVMe
16" Full HD Display
Win11 Pro
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
На странице:
https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/
нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов.
. . .
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Статья исключительно для начинающих. Подходы оригинальностью не блещут.
В век Веб все очень привыкли к дизайну Single-Page-Application .
Быстренько разберем подход "на фреймах".
Мы делаем одну. . .
— Расскажи мне о Мире, бродяга,
Ты же видел моря и метели.
Как сменялись короны и стяги,
Как эпохи стрелою летели.
- Этот мир — это крылья и горы,
Снег и пламя, любовь и тревоги,
И бескрайние. . .