In this article, we will look at a way to give the user the ability to upload any files, such as textures. And let's touch on the topic of launching JS functions from C# within Unity.
The standard way to add js scripts to a project is as follows:
Create a Plugins folder, it's a special folder for plugins .
Create a .jslib file that will contain our JS code.
Functions from JS can be called via:
[DllImport("__Internal")] static extern void [JSFunctionName](); , where [JSFunctionName] is the name of the function from .jslib .
C# Methods can be called from JS via a GameObject name and a method name:
MyGameInstance.SendMessage('MyGameObject', 'MyFunction', [var]); , where MyGameObject is the name of a game object, MyFunction is the name of a method in any of components, [var] is a number or a string that will be passed to the method. Works like GameObject.SendMessage() .
In .jslib, it is mandatory to add functions to the main library using mergeInto(), examples:
mergeInto(LibraryManager.library,
{
// Your code here
Hello: function () {
window.alert("Hello, world!");
}
});
Or like this:
var SomeObject = {
// Your code here
Hello: function () {
window.alert("Hello, world!");
}
};
mergeInto(LibraryManager.library, SomeObject);
See the Unity documentation for more details .
Advice
If you use Visual Studio, then I advise you to add an association with a JavaScript editor for .jslib files.
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/3d5/14a/81e/3d514a81e2178919a4ef66a81287d3b5.png)
We reviewed the basic information. Now let's start implementing feature that the user can upload a texture, for example for an avatar.
To request a file, we need a js script that will interact with the browser, since Unity does not provide direct access to the web form through C#. And so, our script will look like this:
Creating .jslib files
In Unity, you can't create a .jslib file from the editor, for this you need to open a folder in the explorer and set the extension of a js file manually, this is long and inconvenient, so let's add the following script that complements the editor to our project:
// Assets/Editor/JSLibFileCreator.cs
using System.IO;
using UnityEditor;
public class JSLibFileCreator
{
[MenuItem("Assets/Create/JS Script", priority = 80)]
private static void CreateJSLibFile()
{
// Script template so that the file is not empty initially
var asset =
"mergeInto(LibraryManager.library,\n" +
"{\n" +
"\t// Your code here\n" +
"});";
// We take the path to the current open folder in the Project window
string path = AssetDatabase.GetAssetPath(Selection.activeObject);
if (path == "")
{
path = "Assets";
}
else if (Path.GetExtension(path) != "")
{
path = path.Replace(Path.GetFileName(AssetDatabase.GetAssetPath(Selection.activeObject)), "");
}
// Creating a .jslib file with a template
ProjectWindowUtil.CreateAssetWithContent(AssetDatabase.GenerateUniqueAssetPath(path + "/JSScript.jslib"), asset);
// Saving Assets
AssetDatabase.SaveAssets();
}
}
Now we can create .jslib files without too much headache, like this:
![You need to right-click in the Project window, select Create and then click on JS Script You need to right-click in the Project window, select Create and then click on JS Script](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/9b8/834/bdf/9b8834bdff0156db6af4230541d49d50.png)
Created file:
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/499/e36/b4a/499e36b4a1613ab9dc206cc698372d4e.png)
If we open it we will see our template:
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/cb8/207/78b/cb820778b1e1104f96ead7a55d8b638f.png)
// Assets/Plugins/WebGL/JSFileUploader.jslib
mergeInto(LibraryManager.library,
{
InitFileLoader: function (callbackObjectName, callbackMethodName) {
// Strings received from C# must be decoded from UTF8
FileCallbackObjectName = UTF8ToString(callbackObjectName);
FileCallbackMethodName = UTF8ToString(callbackMethodName);
// Create an input to take files if there isn't one already
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;
// If the file is not selected, we complete the execution and call unfocus
// Note: If you need to handle the case where the file is not
// selected, then you can call SendMessage and pass
// null, instead ResetFileLoader()
if (files.length === 0) {
ResetFileLoader();
return;
}
console.log('ObjectName: ' + FileCallbackObjectName + ';\nMethodName: ' + FileCallbackMethodName + ';');
SendMessage(FileCallbackObjectName, FileCallbackMethodName, URL.createObjectURL(files[0]));
};
}
console.log('FileLoader initialized!');
},
// This function is called when the button is pressed, because browser protection doesn't skip click() call
// programmatically
RequestUserFile: function (extensions) {
// Decoding the string from UTF 8
var str = UTF8ToString(extensions);
var fileuploader = document.getElementById('fileuploader');
// If for some reason the fileuploader does not exist - set it
// This can happen in Blazor.NET projects
if (fileuploader === null)
InitFileLoader(FileCallbackObjectName, FileCallbackMethodName);
// Set the received extensions
if (str !== null || str.match(/^ *$/) === null)
fileuploader.setAttribute('accept', str);
// Focus on input and click
fileuploader.setAttribute('class', 'focused');
fileuploader.click();
},
// This function is called after the file is received.
// It can be called from RequestUserFile or fileUploader.onchange
// not from C#, which will be faster, but I'm using the call from C# as a mini-example
// of calling a function without parameters
ResetFileLoader: function () {
var fileuploader = document.getElementById('fileuploader');
if (fileuploader) {
// Removing input from focus
fileuploader.setAttribute('class', 'nonfocused');
}
},
});
And let's create a wrapper for convenient use of our js script:
// Assets/Scripts/FileUploader.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;
// Helper component to get a file
public class FileUploader : MonoBehaviour
{
private void Start()
{
// We don't need to delete it on the new scene, because system is singletone
DontDestroyOnLoad(gameObject);
}
// This method is called from JS via SendMessage
void FileRequestCallback(string path)
{
// Sending the received link back to the FileUploaderHelper
FileUploaderHelper.SetResult(path);
}
}
public static class FileUploaderHelper
{
static FileUploader fileUploaderObject;
static Action<string> pathCallback;
static FileUploaderHelper()
{
string methodName = "FileRequestCallback"; // We will not use reflection, so as not to complicate things, hardcode :)
string objectName = typeof(FileUploaderHelper).Name; // But not here
// Create a helper object for the FileUploader system
var wrapperGameObject = new GameObject(objectName, typeof(FileUploader));
fileUploaderObject = wrapperGameObject.GetComponent<FileUploader>();
// Initializing the JS part of the FileUploader system
InitFileLoader(objectName, methodName);
}
/// <summary>
/// Requests a file from the user.
/// Should be called when the user clicks!
/// </summary>
/// <param name="callback">Will be called after the user selects a file, the Http path to the file is passed as a parameter</param>
/// <param name="extensions">File extensions that can be selected, example: ".jpg, .jpeg, .png"</param>
public static void RequestFile(Action<string> callback, string extensions = ".jpg, .jpeg, .png")
{
RequestUserFile(extensions);
pathCallback = callback;
}
/// <summary>
/// For internal use
/// </summary>
/// <param name="path">The path to the file</param>
public static void SetResult(string path)
{
pathCallback.Invoke(path);
Dispose();
}
private static void Dispose()
{
ResetFileLoader();
pathCallback = null;
}
// Below we declare external functions from our .jslib file
[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();
}
And for tests, we will create such a script that will receive a picture and set it as a user avatar:
// Assets/Scripts/AvatarController.cs
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class AvatarController : MonoBehaviour
{
// Link to the UI picture of the avatar in Canvas
public Image avatarImage;
// This method is called by the button (Button component)
public void UpdateAvatar()
{
// Requesting a file from the user
FileUploaderHelper.RequestFile((path) =>
{
// If the path is empty, ignore it.
if (string.IsNullOrWhiteSpace(path))
return;
// Run a coroutine to load an image
StartCoroutine(UploadImage(path));
});
}
// Coroutine for image upload
IEnumerator UploadImage(string path)
{
// This is where the texture will be stored.
Texture2D texture;
// using to automatically call Dispose, create a request along the path to the file
using (UnityWebRequest imageWeb = new UnityWebRequest(path, UnityWebRequest.kHttpVerbGET))
{
// We create a "downloader" for textures and pass it to the request
imageWeb.downloadHandler = new DownloadHandlerTexture();
// We send a request, execution will continue after the entire file have been downloaded
yield return imageWeb.SendWebRequest();
// Getting the texture from the "downloader"
texture = ((DownloadHandlerTexture)imageWeb.downloadHandler).texture;
}
// Create a sprite from a texture and pass it to the avatar image on the UI
avatarImage.sprite = Sprite.Create(
texture,
new Rect(0.0f, 0.0f, texture.width, texture.height),
new Vector2(0.5f, 0.5f));
}
}
And also create a small scene:
![The main parts are AvatarImage and LoadButton. The main parts are AvatarImage and LoadButton.](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/b6f/db3/438/b6fdb3438913480bab7eab9327505f21.png)
Results of use on different browsers
Edge
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/f27/71a/244/f2771a244952fc36913892c07de6d6a8.png)
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/d9a/6e0/97a/d9a6e097ae4f3f445d7443be16bec6e0.png)
Chrome
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/3e4/f35/37f/3e4f3537f5df6750cec0f28d98f689dc.png)
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/82d/d0d/6a0/82dd0d6a0b80c554dc21bbb6e4b02611.png)
Firefox
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/8dc/406/0f1/8dc4060f1d9de30e7bf2b9540b776956.png)
![](https://webcf.waybackmachine.org/web/20220829115711/https://habrastorage.org/r/w1560/getpro/habr/upload_files/4b1/e8c/494/4b1e8c494248db324a746963279c2899.png)
As a result, we have a system for querying the user's files, which returns the path to the selected file in 1 function call:
Action<string> callback = (str) => { /* Your file handler code here*/ };
FileUploaderHelper.RequestFile(callback);
// Or so, if we need not pictures, but other, special files:
FileUploaderHelper.RequestFile(callback, ".txt, .docx, .csv");
Thank you all for your attention, I hope my articles help you in your projects! I will be glad to additions and criticism.
Code on GitHub:
Translated for TechNation GlobalTalent Visa.