Форум программистов, компьютерный форум, киберфорум
Storm23
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Добавление скриптинга и динамических аддонов в Unity (часть 2)

Запись от Storm23 размещена 30.05.2019 в 17:10
Показов 23067 Комментарии 1
Метки c#, unity

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

В текущем варианте скрипты можно разрабатывать автономно. Для этого не нужен ни исходный код проекта, ни сам Unity. Для разработки аддона нужно только VisualStudio, файл API.dll и откомпилированная версия самой игры.

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

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

Отслеживание изменений плагина в рантайме

Для отслеживания факта изменения плагина можно использовать класс FileSystemWatcher. После того, как файл плагина найден - создаем FileSystemWatcher и начинаем отслеживать его изменения. Как только они возникли - будем загружать аддон снова.

Класс PluginsController будет выглядеть следующим образом:

PluginsController
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using System;
using UnityEngine;
using Scripts;
using System.Linq;
using System.IO;
using System.Reflection;
 
public class PluginsController : MonoBehaviour
{
    ScriptingBase script;
    FileSystemWatcher watcher;
 
    private void 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);
 
        //ищем все классы, унаследованные от ScriptingBase
        foreach (var type in ass.GetTypes())
        {
            //этот тип унаследован от ScriptingBase?
            if (typeof(ScriptingBase).IsAssignableFrom(type))
            // не абстрактный?
            if (!type.IsAbstract)
            {
                //создаем экземпляр скрипта
                script = Activator.CreateInstance(type) as ScriptingBase;
                break;
            }
        }
    }
 
    private void Update()
    {
        //обновляем скрипт
        if (script != null)
            script.Update();
    }
}


Оно БЫ работало, если бы не много всяких НО.

Что бы не тянуть кота за хвост, опишу сразу все проблемы, которые возникнут.

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

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

Тем не менее, обе проблемы решаемы.

Решение проблемы с изменением dll

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

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

Решение проблемы с невозможностью повторной загрузки dll

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

Возможный выход - менять каждый раз имя проекта в VS. Но это очень неудобно.

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

Нажмите на изображение для увеличения
Название: Скриншот 2019-05-30 16.19.36.png
Просмотров: 825
Размер:	92.9 Кб
ID:	5375

Если найти эту последовательность внутри файла и заменить на случайные буквы, то имя ассембли поменяется!
Таким образом нам нужно сделать следующее:
1) Открыть файл ассембли, найти строку, совпадающую с именем файла и заменить это строку на случайную последовательность символов.
2) Сохранить измененную длл во временную папку под случайным именем.
3) Подгрузить измененную длл в движок игры.

Для удобства, сделаем отдельный класс DllHelper, который будет заниматься манипуляциями с длл:

DllHelper
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System.IO;
using System.Text;
 
class DllHelper
{
    static System.Random rnd = new System.Random();
 
    // Изменение внутреннего имени ассембли и копирование ее во временный файл
    public static string 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;
    }
 
    static int FindSequence(byte[] self, byte[] candidate)
    {
        for (int i = 0; i < self.Length; i++)
        if (IsMatch(self, i, candidate))
            return i;
 
        return -1;
    }
 
    static bool IsMatch(byte[] array, int position, byte[] candidate)
    {
        if (candidate.Length > (array.Length - position))
            return false;
 
        for (int i = 0; i < candidate.Length; i++)
            if (array[position + i] != candidate[i])
                return false;
 
        return true;
    }
}


А в классе PluginsController будем вызывать метод CopyAndChangeStrongNameOfDll перед тем как открыть файл плагина:

C#
1
2
3
4
5
6
7
        //меняем внутренне имя и копируем длл во временный файл
        var temp = DllHelper.CopyAndChangeStrongNameOfDll(file);
 
        //загружаем длл
        var ass = Assembly.LoadFile(temp);
 
        //...
Полный код PluginsController выглядит так:

PluginsController
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using System;
using UnityEngine;
using Scripts;
using System.Linq;
using System.IO;
using System.Reflection;
 
public class PluginsController : MonoBehaviour
{
    ScriptingBase script;
    FileSystemWatcher watcher;
    bool needToReloadAddon;
    string addonFile;
 
    private void 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);
 
        //ищем все классы, унаследованные от ScriptingBase
        foreach (var type in ass.GetTypes())
        {
            //этот тип унаследован от ScriptingBase?
            if (typeof(ScriptingBase).IsAssignableFrom(type))
            // не абстрактный?
            if (!type.IsAbstract)
            {
                //создаем экземпляр скрипта
                script = Activator.CreateInstance(type) as ScriptingBase;
                break;
            }
        }
    }
 
    private void Update()
    {
        //если нужно перезагрузить аддон - перезагружаем
        if (needToReloadAddon)
        {
            LoadAddon(addonFile);
            needToReloadAddon = false;
        }
 
        //обновляем скрипт
        if (script != null)
            script.Update();
    }
}


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

Вот как это происходит:


Окончательный код проекта:
Вложения
Тип файла: zip ScriptingExample 3.zip (276.5 Кб, 511 просмотров)
Метки c#, unity
Размещено в Unity
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Система плагинов как для Wf так и для Unity это просто космос.
    Было бы круто увидеть перекомпиляцию приложения "на лету" и побольше бы подобных статеек
    Запись от anomal6 размещена 28.09.2021 в 09:42 anomal6 вне форума
 
Новые блоги и статьи
Особенности gif сгенерированных в Си
russiannick 21.10.2026
Рассмотрим генерацию gif файлов, которые в Си имеют свои особенности. Gif представляет собой графический формат популярный в интернете. Посмотрим на палитру. Gif файл сгенерированный в си отличается. . .
FPS контроллер с анимированными Y-Bot и 3D-звуком miniaudio (Box3D, Recast, BehaviorTree.CPP, ozz-animation, ImGui, OpenGL и SDL3)
8Observer8 18.10.2026
Содержание блога | English Version Полное мультиплатформенное руководство по стартовому проекту с FPS-контроллером игрока, анимированными роботами Y-Bot с 4-слойным блендингом, 3D-физикой Box3D,. . .
Локальный http сервер
Argus19 17.10.2026
Локальный http сервер На работе, на двух локальных компьютерах круглосуточно работают программы опроса приборов с записью в книги Excel,которые раз в сутки записываются на сервер компании. Т. к. . . .
Копировать Windows на новый диск на UEFI
damix 17.10.2026
https:/ / uriesk. wordpress. com/ 2016/ 11/ 01/ move-window-10-to-ssd-on-a-uefi-dualboot-system-with-basic-linux-tools/ TL DR Вставляем диск, а старый подключаем через переходник, грузимся с лайва. . .
ИИ не может найти нужный язык в списке
Supersumestria 05.10.2026
Я ему даю вот такое изображение и прошу найти и подчеркнуть немецкий язык. Возвращает он вот это: https:/ / i. **********/ vqBWLe2. png Нужную строчку в 3й колонке просто выдумал. . Это. . .
Новая последняя моя музыка в SUNO
zorxor 05.10.2026
Здравствуйте, дорогие мои друзья! С большой радостью я хотел бы представить вам свою новую последнею музыку, которую сгенерировала мне по моей просьбе нейросеть SUNO. С уважением, zorxor. Это. . .
Программный домашний кинотеатр
russiannick 27.09.2026
Сподобился на программный домашний кинотеатр. В качестве ЯВУ по традиции выбрал js. В помощники взял Яндекс-Алису. Было создано три зала на разные интересы. исторические и ретро сериал Хичкок. . .
Беседа с ИИ о программистах, недопускающих к созданию и правке кода генеративные ИИ и причины этого
zorxor 21.09.2026
Раньше я радовался или получал некоторые эмоции, пусть небольшие, но всё же, от самого процесса написания кода, рекомпиляции и запуска, видя постепенное развитие программы и прочее. А теперь лень. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru