Unity: Выбор и загрузка файлов пользователем на WebGL сборке

от автора

В этой статье мы рассмотрим способ как дать пользователю возможность загружать какие-либо файлы, к примеру текстуры. И немного затронем тему запуска JS функций из C# в рамках Unity.

Стандартный способ добавления js скриптов в проект следующий:

  1. Создать папку Plugins, это специальная папка для плагинов.

  2. Создать файл .jslib, в котором будет содержаться наш JS код.

  3. Функции из JS можно вызывать через:

    [DllImport(«__Internal»)] static extern void [JSFunctionName](); , где [JSFunctionName] — название функции из .jslib .

  4. По стандарту, методы из C# в JS можно вызывать через имя GameObject и название метода:

    MyGameInstance.SendMessage(‘MyGameObject’, ‘MyFunction’, [var]); , где MyGameObject — имя игрового объекта, MyFunction — имя метода в любом из компонентов, [var] — число или строка, которая будет передана в метод. Работает как GameObject.SendMessage().

В .jslib обязательно нужно добавлять функции в основную библиотеку, при помощи mergeInto(), примеры:

mergeInto(LibraryManager.library,  { // Your code here   Hello: function () {     window.alert("Hello, world!");   } });

Или следующим образом:

var SomeObject = { // Your code here   Hello: function () {     window.alert("Hello, world!");   } };  mergeInto(LibraryManager.library, SomeObject);

Подробнее говорится в документации Unity.

Совет

Если вы пользуетесь Visual Studio, то советую добавить для .jslib файлов ассоциацию с JavaScript редактором

Мы разобрали базу, на случай, если Вы не знакомы с использованием JS скриптов из C# на Unity. Теперь приступим к реализации получения текстуры, которую пользователь сможет загрузить, к примеру для аватара.

Для запроса файла нам необходим js скрипт, который будет взаимодействовать с браузером, так как Unity не предоставляет прямого доступа к веб-форме через C#. И так, наш скрипт будет выглядеть следующим образом:

Создание .jslib файлов

В Unity нельзя создать .jslib файл из редактора, для этого нужно открывать папку в проводнике и задавать расширение вручную, это долго и не удобно, поэтому добавим следующий скрипт, дополняющий редактор, в наш проект:

// Assets/Editor/JSLibFileCreator.cs using System.IO; using UnityEditor;  public class JSLibFileCreator {     [MenuItem("Assets/Create/JS Script", priority = 80)]     private static void CreateJSLibFile()     {       // Шаблон скрипта, что бы файл не был пустым изначально         var asset =             "mergeInto(LibraryManager.library,\n" +             "{\n" +             "\t// Your code here\n" +             "});"; // Берем путь до текущей открытой папки в окне Project         string path = AssetDatabase.GetAssetPath(Selection.activeObject);         if (path == "")         {             path = "Assets";         }         else if (Path.GetExtension(path) != "")         {             path = path.Replace(Path.GetFileName(AssetDatabase.GetAssetPath(Selection.activeObject)), "");         } // Создаем .jslib файл с шаблоном         ProjectWindowUtil.CreateAssetWithContent(AssetDatabase.GenerateUniqueAssetPath(path + "/JSScript.jslib"), asset);         // Сохраняем ассеты       AssetDatabase.SaveAssets();     } } 

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

Нужно нажать ПКМ в окне Project, выбрать Create и затем нажать на JS Script
Нужно нажать ПКМ в окне Project, выбрать Create и затем нажать на JS Script

Полученный файл:

Откроем его, и увидим наш шаблон:

// Assets/Plugins/WebGL/JSFileUploader.jslib mergeInto(LibraryManager.library,     {         InitFileLoader: function (callbackObjectName, callbackMethodName) { // Полученные из C# строки необходимо декодировать из UTF8             FileCallbackObjectName = UTF8ToString(callbackObjectName);             FileCallbackMethodName = UTF8ToString(callbackMethodName);            // Создаем input для взятия файлов, если такого еще нет             var fileuploader = document.getElementById('fileuploader');             if (!fileuploader) {                 console.log('Creating fileuploader...');                 fileuploader = document.createElement('input');                 fileuploader.setAttribute('style', 'display:none;');                 fileuploader.setAttribute('type', 'file');                 fileuploader.setAttribute('id', 'fileuploader');                 fileuploader.setAttribute('class', 'nonfocused');                 document.getElementsByTagName('body')[0].appendChild(fileuploader);                  fileuploader.onchange = function (e) {                     var files = e.target.files;                    // Если файл не выбран - завершаем выполнение и вызываем unfocus                   // Пометка: Если необходимо обрабатывать случай, когда файл не                   // выбран, то тут можно вызывать SendMessage и передавать ему                   // null, вместо ResetFileLoader() if (files.length === 0) {                         ResetFileLoader();                         return;                     }                                        console.log('ObjectName: ' + FileCallbackObjectName + ';\nMethodName: ' + FileCallbackMethodName + ';');                     SendMessage(FileCallbackObjectName, FileCallbackMethodName, URL.createObjectURL(files[0]));                 };             }              console.log('FileLoader initialized!');         },   // Эта функция вызывается на нажатие кнопки, т.к. защита браузера не пропускает вызов click()   // программно         RequestUserFile: function (extensions) {           // Переводим строку из UTF8             var str = UTF8ToString(extensions);             var fileuploader = document.getElementById('fileuploader');            // Если по каким-то причинам fileuploader не существует - задаем его           // Это может случится в проектах Blazor.NET             if (fileuploader === null)                 InitFileLoader(FileCallbackObjectName, FileCallbackMethodName);            // Задаем полученные расширения             if (str !== null || str.match(/^ *$/) === null)                 fileuploader.setAttribute('accept', str);            // Фокус на инпут и клик             fileuploader.setAttribute('class', 'focused');             fileuploader.click();         },    // Эта функция вызывается после получения файла   // Её можно вызывать из RequestUserFile или fileUploader.onchange   // а не из C#, что будет быстрее, но я использую вызов из C# как мини-пример   // вызова функции без параметров         ResetFileLoader: function () {             var fileuploader = document.getElementById('fileuploader');              if (fileuploader) {               // Убираем инпут из фокуса                 fileuploader.setAttribute('class', 'nonfocused');             }         },     });

И создадим обертку, для удобного использования js скрипта:

// Assets/Scripts/FileUploader.cs using System; using System.Runtime.InteropServices; using UnityEngine;  // Компонент - помошник, для получения файла public class FileUploader : MonoBehaviour {     private void Start()     {     // Делаем его неуничтожимым         DontDestroyOnLoad(gameObject);     }      // Этот метод вызывается из JS через SendMessage     void FileRequestCallback(string path)     {     // Отсылаем полученную ссылку обратно в FileUploaderHelper         FileUploaderHelper.SetResult(path);     } }  public static class FileUploaderHelper {     static FileUploader fileUploaderObject;     static Action<string> pathCallback;      static FileUploaderHelper()     {         string methodName = "FileRequestCallback"; // Не будем использовать рефлекцию, чтобы не усложнять, захардкодим :)         string objectName = typeof(FileUploaderHelper).Name; // А здесь используем        // Создаем объект - помошник для системы FileUploader         var wrapperGameObject = new GameObject(objectName, typeof(FileUploader));         fileUploaderObject = wrapperGameObject.GetComponent<FileUploader>();        // Инициализируем JS часть системы FileUploader         InitFileLoader(objectName, methodName);     }      /// <summary>     /// Запрашивает файл у пользователя.     /// Должен вызываться при клике пользователя!     /// </summary>     /// <param name="callback">Будет вызван после выбора файла пользователем, в качестве параметра передается Http путь к файлу</param>     /// <param name="extensions">Расширения файлов, которые можно выбрать, пример: ".jpg, .jpeg, .png"</param>     public static void RequestFile(Action<string> callback, string extensions = ".jpg, .jpeg, .png")     {         RequestUserFile(extensions);         pathCallback = callback;     }      /// <summary>     /// Для внутреннего использования     /// </summary>     /// <param name="path">Путь к файлу</param>     public static void SetResult(string path)     {         pathCallback.Invoke(path);         Dispose();     }      private static void Dispose()     {         ResetFileLoader();         pathCallback = null;     }    // Ниже мы объявляем внешнии функции из нашего .jslib файла     [DllImport("__Internal")]     private static extern void InitFileLoader(string objectName, string methodName);      [DllImport("__Internal")]     private static extern void RequestUserFile(string extensions);      [DllImport("__Internal")]     private static extern void ResetFileLoader(); } 

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

// Assets/Scripts/AvatarController.cs using System.Collections; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI;  public class AvatarController : MonoBehaviour { // Ссылка на UI картинку аватара в Canvas     public Image avatarImage;      // Этот метод вызывается кнопкой (Button компонент)     public void UpdateAvatar()     {     // Запрашиваем файл у пользователя         FileUploaderHelper.RequestFile((path) =>          {         // Если путь пустой - игнорируем             if (string.IsNullOrWhiteSpace(path))                 return;              // Запускаем корутину для загрузки картинки             StartCoroutine(UploadImage(path));             });     }      // Корутина для загрузки картинки     IEnumerator UploadImage(string path)     {     // Тут будет хранится текстура         Texture2D texture;          // using для автоматического вызова Dispose, создаем запрос по пути к файлу         using (UnityWebRequest imageWeb = new UnityWebRequest(path, UnityWebRequest.kHttpVerbGET))         { // Задаем "скачиватель" для текстур и передаем запросу             imageWeb.downloadHandler = new DownloadHandlerTexture();              // Отправляем запрос, выполнение продолжится после выгрузки всего файла             yield return imageWeb.SendWebRequest();              // Получаем текстуру из "скачивателя"             texture = ((DownloadHandlerTexture)imageWeb.downloadHandler).texture;         }  // Создаем спрайт из текстуры и передаем в картинку аватара на UI         avatarImage.sprite = Sprite.Create(             texture,              new Rect(0.0f, 0.0f, texture.width, texture.height),              new Vector2(0.5f, 0.5f));     } } 

И так же создадим небольшую сценку:

Главные части - AvatarImage и Button.
Главные части — AvatarImage и Button.
Результат использования на разных браузерах
Edge

Chrome

Firefox

В результате у нас есть система для запроса файлов пользователя, которая возвращает путь к выбранному файлу через 1 вызов функции:

Action<string> callback = (str) => { /* Your file handler code here*/ }; FileUploaderHelper.RequestFile(callback);  // Или так, если нам нужны не картинки, а другие, особые файлы:  FileUploaderHelper.RequestFile(callback, ".txt, .docx, .csv");

Благодарю всех за внимание, надеюсь мои статьи помогают Вам в Ваших проектах! Буду рад дополнениям и критике, но хочу оправдаться, что система была вырезана из моего проекта, а не написана с нуля, поэтому тут могут быть лишние части.

Код на GitHub:

AlexMorOR/Unity-UserFileUploader: There is a script which allow you to request files from user. (github.com)


ссылка на оригинал статьи https://habr.com/ru/post/684772/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *