В этой статье мы рассмотрим способ как дать пользователю возможность загружать какие-либо файлы, к примеру текстуры. И немного затронем тему запуска JS функций из C# в рамках Unity. В результате мы сможем открывать окно выбора файлов всего одним вызовом функции.
Стандартный способ добавления js скриптов в проект следующий:
Создать папку Plugins, это специальная папка для плагинов.
Создать файл .jslib, в котором будет содержаться наш JS код.
Функции из JS можно вызывать через:
[DllImport("__Internal")] static extern void [JSFunctionName](); , где [JSFunctionName] - название функции из .jslib .
По стандарту, методы из 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 файлы без лишней головной боли, вот так:
Полученный файл:
Откроем его, и увидим наш шаблон:
// 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()
{
// Нам не нужно его удалять на новой сцене, т.к. система Singletone
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));
}
}
И так же создадим небольшую сценку:
Результаты использования на разных браузерах
Edge
Chrome
Firefox
В результате у нас есть система для запроса файлов пользователя, которая возвращает путь к выбранному файлу через 1 вызов функции:
Action<string> callback = (str) => { /* Your file handler code here*/ };
FileUploaderHelper.RequestFile(callback);
// Или так, если нам нужны не картинки, а другие, особые файлы:
FileUploaderHelper.RequestFile(callback, ".txt, .docx, .csv");
Благодарю всех за внимание, надеюсь мои статьи помогают Вам в Ваших проектах! Буду рад дополнениям и критике, но хочу оправдаться, что система была вырезана из моего проекта, а не написана с нуля, поэтому тут могут быть лишние части.
Код на GitHub: