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/


Интересные статьи

Интересные статьи

В нашем мире мобильных гаджетов, сфокусированных на приложениях, уже не принято говорить о веб-сайтах. Классические веб-сайты служат в основном для информационн...
Введение В данном посте будет описано создание простого упаковщика исполняемых файлов под linux x86_64. Предполагается, что читатель знаком с языком программирования си, языком ассемблера для ар...
Автор статьи, перевод которой мы сегодня публикуем, хочет рассказать о том, почему он выполняет предварительную загрузку шрифтов даже тогда, когда не должен этого делать. Когда он создавал тот...
Этот пост будет из серии, об инструментах безопасности, которые доступны в Битриксе сразу «из коробки». Перечислю их все, скажу какой инструмент в какой редакции Битрикса доступен, кратко и не очень р...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...