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

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

Запись от Storm23 размещена 30.05.2019 в 17:10

В первой части статьи нам удалось загрузить и заставить работать аддон, который был создан отдельно от основного проекта 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
Просмотров: 55
Размер:	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 Кб, 11 просмотров)
Размещено в Unity
Просмотров 107 Комментарии 0
Всего комментариев 0
Комментарии
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru