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

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

Запись от Storm23 размещена 30.05.2019 в 17:10
Показов 20963 Комментарии 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
Просмотров: 660
Размер:	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 Кб, 371 просмотров)
Метки c#, unity
Размещено в Unity
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Система плагинов как для Wf так и для Unity это просто космос.
    Было бы круто увидеть перекомпиляцию приложения "на лету" и побольше бы подобных статеек
    Запись от anomal6 размещена 28.09.2021 в 09:42 anomal6 вне форума
 
Новые блоги и статьи
Новый ноутбук
volvo 07.12.2025
Всем привет. По скидке в "черную пятницу" взял себе новый ноутбук Lenovo ThinkBook 16 G7 на Амазоне: Ryzen 5 7533HS 64 Gb DDR5 1Tb NVMe 16" Full HD Display Win11 Pro
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга, Ты же видел моря и метели. Как сменялись короны и стяги, Как эпохи стрелою летели. - Этот мир — это крылья и горы, Снег и пламя, любовь и тревоги, И бескрайние. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru